src/locus_mmdb_check_journal.erl

%% Copyright (c) 2021-2024 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_mmdb_check_journal).

%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------

-export(
   [new/0,
    bad_node_index_in_tree/4,
    bad_data_index_in_tree/5,
    excessively_long_path_in_tree/4,
    loop_in_data_section/2,
    invalid_position_in_data_section/3,
    bad_chunk_in_data_section/4,
    map_key_of_wrong_type_in_data_section/4,
    invalid_utf8_string_in_data_section/5,
    unprintable_utf8_string_in_data_section/4,
    check/1,
    delete/1
   ]).

%% ------------------------------------------------------------------
%% Record and Type Definitions
%% ------------------------------------------------------------------

-record(journal, {
          error_tables :: tuple(),
          warning_tables :: tuple()
         }).
-opaque t() :: #journal{}.
-export_type([t/0]).

%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------

-spec new() -> t().
new() ->
    #journal{
       error_tables = new_tables(),
       warning_tables = new_tables()
      }.

-spec bad_node_index_in_tree(t(), non_neg_integer(), string(), [non_neg_integer()])
        -> true.
bad_node_index_in_tree(Journal, NodeIndex, Prefix, Path) ->
    Error = bad_node_index_in_tree(NodeIndex, Prefix, Path),
    register_error(Journal, Error).

-spec bad_data_index_in_tree(t(), non_neg_integer(), term(),
                             string(), [non_neg_integer()])
        -> true.
bad_data_index_in_tree(Journal, DataIndex, Why, Prefix, Path) ->
    Error = bad_data_index_in_tree(DataIndex, Why, Prefix, Path),
    register_error(Journal, Error).

-spec excessively_long_path_in_tree(t(), non_neg_integer(), string(), [non_neg_integer()])
        -> true.
excessively_long_path_in_tree(Journal, NodeIndex, Prefix, Path) ->
    Error = excessively_long_path_in_tree(NodeIndex, Prefix, Path),
    register_error(Journal, Error).

-spec loop_in_data_section(t(), [{non_neg_integer(), term()}, ...]) -> true.
loop_in_data_section(Journal, Path) ->
    Error = loop_in_data_section(Path),
    register_error(Journal, Error).

-spec invalid_position_in_data_section(t(), non_neg_integer(),
                                       [{non_neg_integer(), term()}, ...])
        -> true.
invalid_position_in_data_section(Journal, Position, Path) ->
    Error = invalid_position_in_data_section(Position, Path),
    register_error(Journal, Error).

-spec bad_chunk_in_data_section(t(), non_neg_integer(), term(),
                                [{non_neg_integer(), term()}, ...])
        -> true.
bad_chunk_in_data_section(Journal, Position, Why, Path) ->
    Error = bad_chunk_in_data_section(Position, Why, Path),
    register_error(Journal, Error).

-spec map_key_of_wrong_type_in_data_section(t(), non_neg_integer(), term(),
                                            [{non_neg_integer(), term()}])
        -> true.
map_key_of_wrong_type_in_data_section(Journal, Position, Key, Path) ->
    Error = map_key_of_wrong_type_in_data_section(Position, Key, Path),
    register_error(Journal, Error).

-spec invalid_utf8_string_in_data_section(t(), non_neg_integer(), binary(),
                                          term(), [{non_neg_integer(), term()}])
        -> true.
invalid_utf8_string_in_data_section(Journal, Position, OriginalData, ValidationError, Path) ->
    Error = invalid_utf8_string_in_data_section(Position, OriginalData, ValidationError, Path),
    register_error(Journal, Error).

-spec unprintable_utf8_string_in_data_section(t(), non_neg_integer(), unicode:unicode_binary(),
                                              [{non_neg_integer(), term()}])
        -> true.
unprintable_utf8_string_in_data_section(Journal, Position, Value, Path) ->
    Warning = unprintable_utf8_string_in_data_section(Position, Value, Path),
    register_warning(Journal, Warning).

-spec check(t()) -> #{errors := [locus_mmdb_check:error()],
                      warnings := [locus_mmdb_check:warning()]}.
check(Journal) ->
    #{errors => tables_to_list(Journal#journal.error_tables),
      warnings => tables_to_list(Journal#journal.warning_tables)}.

-spec delete(t()) -> ok.
delete(Journal) ->
    delete_tables(Journal#journal.error_tables),
    delete_tables(Journal#journal.warning_tables).

%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------

-spec bad_node_index_in_tree(non_neg_integer(), string(), [non_neg_integer()])
        -> locus_mmdb_check:bad_node_index_in_tree().
bad_node_index_in_tree(NodeIndex, Prefix, Path) ->
    {bad_node_index_in_tree, NodeIndex, #{under => #{prefix => Prefix,
                                                     path => Path}}}.

-spec bad_data_index_in_tree(non_neg_integer(), term(), string(), [non_neg_integer()])
        -> locus_mmdb_check:bad_data_index_in_tree().
bad_data_index_in_tree(DataIndex, Why, Prefix, Path) ->
    {bad_data_index_in_tree, DataIndex, #{why => Why,
                                          under => #{prefix => Prefix,
                                                     path => Path}}}.

-spec excessively_long_path_in_tree(non_neg_integer(), string(), [non_neg_integer()])
        -> locus_mmdb_check:excessively_long_path_in_tree().
excessively_long_path_in_tree(NodeIndex, Prefix, Path) ->
    {excessively_long_path_in_tree, NodeIndex, #{under => #{prefix => Prefix,
                                                            path => Path}}}.

-spec loop_in_data_section([{non_neg_integer(), term()}, ...])
        -> locus_mmdb_check:loop_in_data_section().
loop_in_data_section(Path) ->
    {loop_in_data_section, #{path => Path}}.

-spec invalid_position_in_data_section(non_neg_integer(), [{non_neg_integer(), term()}, ...])
        -> locus_mmdb_check:invalid_position_in_data_section().
invalid_position_in_data_section(Position, Path) ->
    {invalid_position_in_data_section, #{which => Position,
                                         under => #{path => Path}}}.

-spec bad_chunk_in_data_section(non_neg_integer(), term(), [{non_neg_integer(), term()}, ...])
        -> locus_mmdb_check:bad_chunk_in_data_section().
bad_chunk_in_data_section(Position, Why, Path) ->
    {bad_chunk_in_data_section, #{position => Position,
                                  why => Why,
                                  under => #{path => Path}}}.

-spec map_key_of_wrong_type_in_data_section(non_neg_integer(), term(),
                                            [{non_neg_integer(), term()}])
        -> locus_mmdb_check:map_key_of_wrong_type_in_data_section().
map_key_of_wrong_type_in_data_section(Position, Key, Path) ->
    {map_key_of_wrong_type_in_data_section, #{position => Position,
                                              key => Key,
                                              under => #{path => Path}}}.

-spec invalid_utf8_string_in_data_section(non_neg_integer(), binary(),
                                          term(), [{non_neg_integer(), term()}])
        -> locus_mmdb_check:invalid_utf8_string_in_data_section().
invalid_utf8_string_in_data_section(Position, OriginalData, ValidationError, Path) ->
    {invalid_utf8_string_in_data_section, #{position => Position,
                                            original_data => OriginalData,
                                            error => ValidationError,
                                            under => #{path => Path}}}.

-spec unprintable_utf8_string_in_data_section(non_neg_integer(),
                                              unicode:unicode_binary(),
                                              [{non_neg_integer(), term()}])
        -> locus_mmdb_check:unprintable_utf8_string_in_data_section().
unprintable_utf8_string_in_data_section(Position, Value, Path) ->
    {unprintable_utf8_string_in_data_section, #{position => Position,
                                                value => Value,
                                                under => #{path => Path}}}.

register_error(#journal{error_tables = Tables}, Error) ->
    Table = pick_table(Tables),
    ets:insert(Table, {make_ref(), Error}).

register_warning(#journal{warning_tables = Tables}, Error) ->
    Table = pick_table(Tables),
    ets:insert(Table, {make_ref(), Error}).

new_tables() ->
    Amount = erlang:system_info(schedulers),
    list_to_tuple([new_table() || _ <- lists:seq(1, Amount)]).

new_table() ->
    ets:new(?MODULE, [public]).

pick_table(Tables) ->
    TableIndex = erlang:system_info(scheduler_id),
    element(TableIndex, Tables).

tables_to_list(Tables) ->
    lists:foldl(fun prepend_table_to_list/2, [], tuple_to_list(Tables)).

prepend_table_to_list(Table, Acc) ->
    ets:foldl(fun prepend_element_to_list/2, Acc, Table).

prepend_element_to_list({Ref, Element}, Acc)
  when is_reference(Ref) ->
    [Element | Acc].

delete_tables(Tables) ->
    lists:foreach(fun ets:delete/1, tuple_to_list(Tables)).