|
1 | 1 | -module(mod_caps_hash). |
2 | 2 |
|
3 | | --export([is_known_hash_alg/1, generate/2, encode/1]). |
| 3 | +-export([is_known_hash_alg/1, generate/3, encode/2]). |
4 | 4 |
|
5 | | --ignore_xref([encode/1]). % exported for tests |
| 5 | +-ignore_xref([encode/2]). % exported for tests |
| 6 | + |
| 7 | +-type encoded_element() :: {group(), iolist()} | {other, exml:element()}. |
| 8 | +-type group() :: feature | identity | form. |
6 | 9 |
|
7 | 10 | -include("jlib.hrl"). |
8 | 11 |
|
9 | 12 | -spec is_known_hash_alg(mod_caps:hash_alg()) -> boolean(). |
10 | 13 | is_known_hash_alg(Alg) -> |
11 | 14 | hash_function(Alg) =/= unknown_alg. |
12 | 15 |
|
13 | | --spec generate([exml:element()], mod_caps:hash_alg()) -> mod_caps:hash_value(). |
14 | | -generate(Elements, Alg) -> |
15 | | - base64:encode(hash(encode(Elements), Alg)). |
| 16 | +-spec generate([exml:element()], mod_caps:version(), mod_caps:hash_alg()) -> mod_caps:hash_value(). |
| 17 | +generate(Elements, Version, Alg) -> |
| 18 | + base64:encode(hash(encode(Version, Elements), Alg)). |
| 19 | + |
| 20 | +%% Processing and encoding of the disco#info response |
| 21 | + |
| 22 | +-spec encode(mod_caps:version(), [exml:element()]) -> binary(). |
| 23 | +encode(Version, Elements) -> |
| 24 | + EncodedEls = lists:map(fun(Element) -> encode_element(Version, Element) end, Elements), |
| 25 | + case make_groups(EncodedEls) of |
| 26 | + #{other := InvalidElements} when Version =:= v2 -> |
| 27 | + %% Skip elements instead of throwing an error for v1 |
| 28 | + error(#{what => mod_caps_hash_invalid_elements, invalid_elements => InvalidElements}); |
| 29 | + Groups -> |
| 30 | + Result = [encode_group(Version, lists:sort(maps:get(Group, Groups, []))) |
| 31 | + || Group <- ordered_groups(Version)], |
| 32 | + iolist_to_binary(Result) |
| 33 | + end. |
16 | 34 |
|
17 | | --spec encode([exml:element()]) -> binary(). |
18 | | -encode(Elements) -> |
19 | | - EncodedItems = lists:sort(lists:map(fun encode_item/1, Elements)), |
20 | | - iolist_to_binary([Item || {_Priority, Item} <- EncodedItems]). |
| 35 | +-spec make_groups([encoded_element()]) -> #{feature | identity | form => [iolist()], |
| 36 | + other => [exml:element()]}. |
| 37 | +make_groups(EncodedEls) -> |
| 38 | + maps:groups_from_list(fun({Group, _}) -> Group end, fun({_, Value}) -> Value end, EncodedEls). |
21 | 39 |
|
22 | | --spec encode_item(exml:element()) -> {Priority :: pos_integer(), iolist()} | skip. |
23 | | -encode_item(#xmlel{name = ~"identity", |
24 | | - attrs = Attrs = #{~"category" := Category, ~"type" := Type}}) -> |
| 40 | +-spec encode_element(mod_caps:version(), exml:element()) -> encoded_element(). |
| 41 | +encode_element(Version, #xmlel{name = ~"feature", attrs = #{~"var" := Var}}) -> |
| 42 | + {feature, encode_feature(Version, Var)}; |
| 43 | +encode_element(Version, #xmlel{name = ~"identity", |
| 44 | + attrs = Attrs = #{~"category" := Category, ~"type" := Type}}) -> |
25 | 45 | Lang = maps:get(~"xml:lang", Attrs, <<>>), |
26 | 46 | Name = maps:get(~"name", Attrs, <<>>), |
27 | | - {1, [Category, $/, Type, $/, Lang, $/, Name, $<]}; |
28 | | -encode_item(#xmlel{name = ~"feature", attrs = #{~"var" := Var}}) -> |
29 | | - {2, [Var, $<]}; |
30 | | -encode_item(Element) -> |
| 47 | + {identity, encode_identity(Version, Category, Type, Lang, Name)}; |
| 48 | +encode_element(Version, Element) -> |
31 | 49 | maybe |
32 | | - true ?= mongoose_data_forms:is_form(Element), |
33 | | - #{type := ~"result", kvs := KVs, ns := NS} ?= |
34 | | - mongoose_data_forms:parse_form_fields(Element), |
35 | | - {3, [NS, $< | encode_form_fields(KVs)]} |
| 50 | + #{type := ~"result", kvs := KVs, ns := NS} ?= mongoose_data_forms:parse_form(Element), |
| 51 | + {form, encode_form(Version, NS, KVs)} |
36 | 52 | else |
37 | | - _ -> skip % this could happen for a non-conformant or custom info response |
| 53 | + _ -> {other, Element} % this could happen for a non-conformant or custom info response |
38 | 54 | end. |
39 | 55 |
|
40 | | --spec encode_form_fields(mongoose_data_forms:kv_map()) -> iolist(). |
41 | | -encode_form_fields(KVs) -> |
42 | | - [encode_form_field(K, V) || K := V <- maps:iterator(KVs, ordered)]. |
| 56 | +-spec encode_form_fields(mod_caps:version(), mongoose_data_forms:kv_map()) -> iolist(). |
| 57 | +encode_form_fields(Version, KVs) -> |
| 58 | + [encode_form_field(Version, [K | lists:sort(Vs)]) || K := Vs <- maps:iterator(KVs, ordered)]. |
| 59 | + |
| 60 | +%% Version-specific encoding format |
| 61 | + |
| 62 | +ordered_groups(v1) -> [identity, feature, form]; |
| 63 | +ordered_groups(v2) -> [feature, identity, form]. |
| 64 | + |
| 65 | +encode_group(v1, Values) -> Values; |
| 66 | +encode_group(v2, Values) -> [Values, 16#1c]. |
| 67 | + |
| 68 | +encode_identity(v1, Cat, Type, Lang, Name) -> [Cat, $/, Type, $/, Lang, $/, Name, $<]; |
| 69 | +encode_identity(v2, Cat, Type, Lang, Name) -> [Cat, 16#1f, Type, 16#1f, Lang, 16#1f, Name, 16#1f, 16#1e]. |
| 70 | + |
| 71 | +encode_feature(v1, Var) -> [Var, $<]; |
| 72 | +encode_feature(v2, Var) -> [Var, 16#1f]. |
| 73 | + |
| 74 | +encode_form(v1, NS, KVs) -> [NS, $< | encode_form_fields(v1, KVs)]; |
| 75 | +encode_form(v2, NS, KVs) -> [encode_form_fields(v2, KVs#{~"FORM_TYPE" => [NS]}), 16#1d]. |
| 76 | + |
| 77 | +encode_form_field(v1, Values) -> [[Value, $<] || Value <- Values]; |
| 78 | +encode_form_field(v2, Values) -> [[[Value, 16#1f] || Value <- Values], 16#1e]. |
43 | 79 |
|
44 | | --spec encode_form_field(binary(), [binary()]) -> iolist(). |
45 | | -encode_form_field(Key, Values) -> |
46 | | - [[Item, $<] || Item <- [Key | lists:sort(Values)]]. |
| 80 | +%% Hash calculation |
47 | 81 |
|
48 | 82 | -spec hash(binary(), mod_caps:hash_alg()) -> binary(). |
49 | 83 | hash(Data, Alg) -> |
|
0 commit comments