%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2016. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%
-module(asn1_db).
-export([dbstart/1,dbnew/2,dbload/1,dbload/3,dbsave/2,dbput/2,
dbput/3,dbget/2]).
-export([dbstop/0]).
-record(state, {parent, monitor, includes, table}).
%% Interface
dbstart(Includes0) ->
Includes = case Includes0 of
[] -> ["."];
[_|_] -> Includes0
end,
Parent = self(),
undefined = get(?MODULE), %Assertion.
put(?MODULE, spawn_link(fun() -> init(Parent, Includes) end)),
ok.
dbload(Module, Erule, Mtime) ->
req({load, Module, Erule, Mtime}).
dbload(Module) ->
req({load, Module, any, {{0,0,0},{0,0,0}}}).
dbnew(Module, Erule) -> req({new, Module, Erule}).
dbsave(OutFile, Module) -> cast({save, OutFile, Module}).
dbput(Module, K, V) -> cast({set, Module, K, V}).
dbput(Module, Kvs) -> cast({set, Module, Kvs}).
dbget(Module, K) -> req({get, Module, K}).
dbstop() -> Resp = req(stop), erase(?MODULE), Resp.
%% Internal functions
-define(MAGIC_KEY, '__version_and_erule__').
req(Request) ->
DbPid = get(?MODULE),
Ref = erlang:monitor(process,DbPid),
get(?MODULE) ! {{Ref, self()}, Request},
receive
{{Ref,?MODULE}, Reply} ->
erlang:demonitor(Ref,[flush]),
Reply;
{'DOWN',Ref,_,_,Info} ->
exit({db_error,Info})
end.
cast(Request) ->
get(?MODULE) ! Request,
ok.
reply({Ref,From}, Response) ->
From ! {{Ref,?MODULE}, Response},
ok.
init(Parent, Includes) ->
MRef = erlang:monitor(process, Parent),
loop(#state{parent = Parent, monitor = MRef, includes = Includes,
table = ets:new(?MODULE, [])}).
loop(#state{parent = Parent, monitor = MRef, table = Table,
includes = Includes} = State) ->
receive
{set, Mod, K2, V} ->
[{_, Modtab}] = ets:lookup(Table, Mod),
ets:insert(Modtab, {K2, V}),
loop(State);
{set, Mod, Kvs} ->
[{_, Modtab}] = ets:lookup(Table, Mod),
ets:insert(Modtab, Kvs),
loop(State);
{From, {get, Mod, K2}} ->
%% XXX If there is no information for Mod, get_table/3
%% will attempt to load information from an .asn1db
%% file, without comparing its timestamp against the
%% source file. This is known to happen when check_*
%% functions for DER are generated, but it could possibly
%% happen in other circumstances. Ideally, this issue should
%% be rectified in some way, perhaps by ensuring that
%% the module has been loaded (using dbload/4) prior
%% to calling dbget/2.
case get_table(Table, Mod, Includes) of
{ok,Tab} -> reply(From, lookup(Tab, K2));
error -> reply(From, undefined)
end,
loop(State);
{save, OutFile, Mod} ->
[{_,Mtab}] = ets:lookup(Table, Mod),
ok = ets:tab2file(Mtab, OutFile),
loop(State);
{From, {new, Mod, Erule}} ->
[] = ets:lookup(Table, Mod), %Assertion.
ModTableId = ets:new(list_to_atom(lists:concat(["asn1_",Mod])), []),
ets:insert(Table, {Mod, ModTableId}),
ets:insert(ModTableId, {?MAGIC_KEY, info(Erule)}),
reply(From, ok),
loop(State);
{From, {load, Mod, Erule, Mtime}} ->
case ets:member(Table, Mod) of
true ->
reply(From, ok);
false ->
case load_table(Mod, Erule, Mtime, Includes) of
{ok, ModTableId} ->
ets:insert(Table, {Mod, ModTableId}),
reply(From, ok);
error ->
reply(From, error)
end
end,
loop(State);
{From, stop} ->
reply(From, stopped); %% Nothing to store
{'DOWN', MRef, process, Parent, Reason} ->
exit(Reason)
end.
get_table(Table, Mod, Includes) ->
case ets:lookup(Table, Mod) of
[{Mod,Tab}] ->
{ok,Tab};
[] ->
load_table(Mod, any, {{0,0,0},{0,0,0}}, Includes)
end.
lookup(Tab, K) ->
case ets:lookup(Tab, K) of
[] -> undefined;
[{K,V}] -> V
end.
info(Erule) ->
{asn1ct:vsn(),Erule}.
load_table(Mod, Erule, Mtime, Includes) ->
Base = lists:concat([Mod, ".asn1db"]),
case path_find(Includes, Mtime, Base) of
error ->
error;
{ok,ModTab} when Erule =:= any ->
{ok,ModTab};
{ok,ModTab} ->
Vsn = asn1ct:vsn(),
case ets:lookup(ModTab, ?MAGIC_KEY) of
[{_,{Vsn,Erule}}] ->
%% Correct version and encoding rule.
{ok,ModTab};
_ ->
%% Missing key or wrong version/encoding rule.
ets:delete(ModTab),
error
end
end.
path_find([H|T], Mtime, Base) ->
File = filename:join(H, Base),
case filelib:last_modified(File) of
0 ->
path_find(T, Mtime, Base);
DbMtime when DbMtime >= Mtime ->
case ets:file2tab(File) of
{ok,_}=Ret ->
Ret;
_ ->
path_find(T, Mtime, Base)
end;
_ ->
path_find(T, Mtime, Base)
end;
path_find([], _, _) -> error.