diff --git a/src/core/device/hb_device.erl b/src/core/device/hb_device.erl index df9ea5aa9..ee34828ea 100644 --- a/src/core/device/hb_device.erl +++ b/src/core/device/hb_device.erl @@ -4,7 +4,9 @@ -module(hb_device). -export([truncate_args/2, message_to_fun/3, message_to_device/2]). -export([is_direct_key_access/3, is_direct_key_access/4]). +-export([is_reserved/3, is_reserved/4]). -export([find_exported_function/5, is_exported/4, info/2, info/3]). +-include_lib("eunit/include/eunit.hrl"). -include("include/hb.hrl"). -define(DEFAULT_DEVICE, <<"message@1.0">>). @@ -145,13 +147,15 @@ info_handler_to_fun(HandlerMap, Msg, Key, Opts) -> {ok, Exclude} -> case lists:member(Key, Exclude) of true -> - MsgWithoutDevice = - hb_maps:without([<<"device">>], Msg, Opts), - message_to_fun( - MsgWithoutDevice#{ <<"device">> => ?DEFAULT_DEVICE }, - Key, - Opts - ); + MsgWithDefaultDevice = + hb_maps:put(<<"device">>, ?DEFAULT_DEVICE, Msg, Opts), + {Status, _Dev, Func} = + message_to_fun( + MsgWithDefaultDevice, + Key, + Opts + ), + {Status, Func}; false -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)} end; error -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)} @@ -226,6 +230,33 @@ is_exported(#{ exports := Exports }, Key, _Opts) -> ); is_exported(_Info, _Key, _Opts) -> true. +%% @doc Determine if a key is reserved by a device. +%% A key is reserved if it is declared in the device's `info().reserved' list, +%% or if it resolves to a direct device function. Keys handled by a device's +%% default handler are not reserved. +is_reserved(Base, Key, Opts) -> + is_reserved(message_to_device(Base, Opts), Base, Key, Opts). +is_reserved(Dev, Base, Key, Opts) when is_binary(Dev) -> + DevMod = message_to_device(#{ <<"device">> => Dev }, Opts), + is_reserved(DevMod, Base, Key, Opts); +is_reserved(Dev, Base, Key, Opts) -> + Info = info(Dev, Base, Opts), + is_reserved_key(Info, Key, Opts) orelse is_device_key(Dev, Base, Key, Opts). + +%% @doc Check if a key appears in a device's `reserved' info list. +is_reserved_key(Info, Key, Opts) -> + Reserved = hb_maps:get(reserved, Info, [], Opts), + NormReserved = lists:map(fun maybe_normalize_device_key/1, Reserved), + lists:member(maybe_normalize_device_key(Key, existing), NormReserved). + +%% @doc Check if a key is claimed by the device's direct function dispatch. +is_device_key(Dev, Base, Key, Opts) -> + try message_to_fun(Dev, Base, Key, Opts) of + {ok, _, _} -> true; + {add_key, _, _} -> false + catch _:_:_ -> false + end. + %% @doc Normalize an exported key to its canonical atomized form. By default %% new atoms are created if necessary. In practice this is used for keys that %% orinate from a device's `info' response, but _not_ for keys that could be @@ -301,3 +332,34 @@ do_is_direct_key_access(Dev, NormKey, Opts) -> not lists:member(NormKey, Exports ++ ?MESSAGE_KEYS); _ -> false end. + +explicit_reserved_key_test() -> + Trie = #{ <<"device">> => <<"trie@1.0">> }, + ?assert(is_reserved(Trie, <<"node-value">>, #{})), + ?assert(is_reserved(<<"trie@1.0">>, Trie, <<"node-value">>, #{})), + ?assert(is_reserved(Trie, <<"Node-Value">>, #{})), + ?assertNot(is_reserved(Trie, <<"alice">>, #{})). + +message_reserved_key_test() -> + Msg = #{ <<"device">> => <<"message@1.0">> }, + Trie = #{ <<"device">> => <<"trie@1.0">> }, + ?assert(is_reserved(Msg, <<"path">>, #{})), + ?assert(is_reserved(Msg, <<"set">>, #{})), + ?assert(is_reserved(Msg, <<"get">>, #{})), + ?assert(is_reserved(Trie, <<"get">>, #{})), + ?assert(is_reserved(Trie, <<"set">>, #{})), + ?assert(is_reserved(Trie, <<"keys">>, #{})), + ?assertNot(is_reserved(Trie, <<"commit">>, #{})), + ?assertNot(is_reserved(Msg, <<"alice">>, #{})). + +trie_keys_skip_reserved_keys_test() -> + Trie = + #{ + <<"device">> => <<"trie@1.0">>, + <<"node-value">> => ignored, + <<"get">> => ignored, + <<"set">> => ignored, + <<"keys">> => ignored, + <<"alice">> => 1 + }, + ?assertEqual([<<>>, <<"alice">>], lists:sort(hb_ao:keys(Trie, #{}))). diff --git a/src/preloaded/message/dev_message.erl b/src/preloaded/message/dev_message.erl index 90a9a0ff4..78e0b5055 100644 --- a/src/preloaded/message/dev_message.erl +++ b/src/preloaded/message/dev_message.erl @@ -1,9 +1,9 @@ %%% @doc The identity device: For non-reserved keys, it simply returns a key %%% from the message as it is found in the message's underlying Erlang map. %%% Private keys (`priv[.*]') are not included. -%%% Reserved keys are: `id', `commitments', `committers', `keys', `path', -%%% `set', `remove', `get', and `verify'. Their function comments describe the -%%% behaviour of the device when these keys are set. +%%% Reserved keys are: `id', `commitments', `committers', `keys', `path', +%%% `set', `remove', `get', `commit', `committed', and `verify'. Their function +%%% comments describe the behaviour of the device when these keys are set. -module(dev_message). %%% Base AO-Core reserved keys: -export([info/0, keys/1, keys/2]). @@ -18,7 +18,7 @@ -define(DEFAULT_ID_DEVICE, <<"httpsig@1.0">>). -define(DEFAULT_ATT_DEVICE, <<"httpsig@1.0">>). -%% The list of keys that are exported by this device. +%% The list of keys that `set/3' filters before writing message data. -define(DEVICE_KEYS, [ <<"id">>, <<"commitments">>, @@ -30,10 +30,18 @@ <<"verify">> ]). +%% The list of keys that the message device reserves at protocol level. +-define(RESERVED_KEYS, ?DEVICE_KEYS ++ [ + <<"get">>, + <<"commit">>, + <<"committed">> +]). + %% @doc Return the info for the identity device. info() -> #{ - default => fun dev_message:get/4 + default => fun dev_message:get/4, + reserved => ?RESERVED_KEYS }. %% @doc Generate an index page for a message, in the event that the `body' and diff --git a/src/preloaded/message/dev_trie.erl b/src/preloaded/message/dev_trie.erl index 2dae71520..370f89b4b 100644 --- a/src/preloaded/message/dev_trie.erl +++ b/src/preloaded/message/dev_trie.erl @@ -24,20 +24,17 @@ %%% but cannot be properly normalized. -define(RADIX, 256). -%%% @doc Trie node metadata keys that must not be treated as edge labels. --define(RESERVED_KEYS, [ - <<"node-value">>, - <<"device">>, - <<"commitments">>, - <<"priv">>, - <<"hashpath">> -]). - info() -> #{ default => fun get/4, - reserved => ?RESERVED_KEYS - }. + reserved => [ + <<"node-value">>, + <<"device">>, + <<"commitments">>, + <<"priv">>, + <<"hashpath">> + ] + }. keys(Trie, Opts) -> collect_keys(Trie, <<>>, Opts, []). @@ -252,12 +249,12 @@ retrieve(TrieNode, Key, Opts, KeyPrefixSizeAcc) -> %% @doc Get a list of edge labels for a given trie node. edges(TrieNode, _Opts) when not is_map(TrieNode) -> []; edges(TrieNode, Opts) -> - Filtered = hb_maps:without( - ?RESERVED_KEYS, - TrieNode, - Opts - ), - hb_maps:keys(Filtered). + [ + Key + || + Key <- hb_maps:keys(TrieNode, Opts), + not hb_device:is_reserved(?MODULE, TrieNode, Key, Opts) + ]. %% @doc Compute the longest common binary prefix of A and B, comparing chunks of %% N bits.