From b203c63ee9b422a229f4eb9670981d9ae334205d Mon Sep 17 00:00:00 2001 From: Nicolas Berbiche Date: Sun, 24 Sep 2023 17:14:30 -0400 Subject: [PATCH] Implement additional consent (AC) string parsing Use single error type for ac string parsing failure Adds a new record under the name `#addtl_consent{}` to contain the parsed additional consent string. --- include/consent_string.hrl | 6 +++ rebar.config | 2 - src/additional_consent_string.erl | 89 +++++++++++++++++++++++++++++++ test/ac_string_tests.erl | 75 ++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/additional_consent_string.erl create mode 100644 test/ac_string_tests.erl diff --git a/include/consent_string.hrl b/include/consent_string.hrl index 07bb486..d7b7047 100644 --- a/include/consent_string.hrl +++ b/include/consent_string.hrl @@ -38,6 +38,11 @@ publisher_tc :: undefined | consent_segment() }). +-record(addtl_consent, { + version :: pos_integer(), + atp_ids :: [pos_integer()] +}). + -record(consent_segment_entry_disclosed_vendors, { max_vendor_id :: pos_integer(), entries :: range_or_bitfield() @@ -98,6 +103,7 @@ }). -type consent() :: #consent {}. +-type addtl_consent() :: #addtl_consent {}. -type consent_segment() :: #consent_segment {}. diff --git a/rebar.config b/rebar.config index bef8488..3b98d2d 100644 --- a/rebar.config +++ b/rebar.config @@ -32,8 +32,6 @@ ]} ]}. -{project_plugins, [rebar3_lint]}. - {xref_checks, [ deprecated_functions, deprecated_function_calls, diff --git a/src/additional_consent_string.erl b/src/additional_consent_string.erl new file mode 100644 index 0000000..74bbd29 --- /dev/null +++ b/src/additional_consent_string.erl @@ -0,0 +1,89 @@ +-module(additional_consent_string). +-include("consent_string.hrl"). + +-compile([inline]). + +-export([parse/1]). +-ifdef(TEST). +-export([parse_version/1, parse_atp_ids/1]). +-endif. + +-define(KNOWN_SEPARATOR, "~"). + + +-spec parse(binary()) -> + {ok, addtl_consent()} | {error, invalid_ac_string}. + +% @doc Parses an additional consent string, returning the version +% number and the list of numerical ATP ids. +% +% @returns `{ok, #addtl_consent{}}' if the string is valid, +% otherwise an appropriate error is returned. +% +parse(Bin) when is_binary(Bin) -> + case parse_version(Bin) of + {ok, Version, Bin1} -> + case parse_atp_ids(Bin1) of + {ok, AtpIds} -> + {ok, #addtl_consent { + version = Version, + atp_ids = AtpIds + }}; + Error -> Error + end; + Error -> Error + end; +parse(_) -> + {error, invalid_ac_string}. + +parse_version(Bin) -> + parse_version(Bin, <<"">>). + +parse_version(<>, Acc) when Bin >= $0, Bin =< $9 -> + parse_version(Rest, <>); +% Consume the separator token +parse_version(<>, Acc) -> + case safe_binary_to_integer(Acc) of + undefined -> + {error, invalid_ac_string}; + Version -> + {ok, Version, Rest} + end; +parse_version(_, _) -> + {error, invalid_ac_string}. + +parse_atp_ids(Bin) -> + parse_atp_ids(Bin, <<"">>, []). + +parse_atp_ids(<>, Acc, Ids) when Bin >= $0, Bin =< $9 -> + parse_atp_ids(Rest, <>, Ids); +parse_atp_ids(<<".", Rest/binary>>, Acc, Ids) when Rest =/= <<"">> -> + case safe_binary_to_integer(Acc) of + undefined -> + {error, invalid_ac_string}; + Id -> + parse_atp_ids(Rest, <<"">>, [Id | Ids]) + end; +parse_atp_ids(<<_Invalid, _Rest/binary>>, _Acc, _Ids) -> + {error, invalid_ac_string}; +parse_atp_ids(<<"">>, <<"">>, Ids) -> + {ok, lists:reverse(Ids)}; +parse_atp_ids(<<"">>, Acc, Ids) -> + case safe_binary_to_integer(Acc) of + undefined -> + {error, invalid_ac_string}; + Id -> + % Preserve the order of ids + {ok, lists:reverse([Id | Ids])} + end. + + +-spec safe_binary_to_integer(binary()) -> undefined | integer(). +safe_binary_to_integer(Bin) -> + try + binary_to_integer(Bin, 10) + catch + _:_ -> + undefined + end. + diff --git a/test/ac_string_tests.erl b/test/ac_string_tests.erl new file mode 100644 index 0000000..54be745 --- /dev/null +++ b/test/ac_string_tests.erl @@ -0,0 +1,75 @@ +-module(ac_string_tests). +-include("consent_string.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-define(_assertMatchAtp(A, B), + ?_assertMatch({ok, (A)}, parse_atp_ids((B)))). +-define(_assertMatchVersion(A, B), + ?_assertMatch({ok, (A), _}, parse_version((B)))). +-define(_assertErrorVersion(B), + ?_assertMatch({error, invalid_ac_string}, parse_version((B)))). +-define(_assertErrorAtp(B), + ?_assertMatch({error, invalid_ac_string}, parse_atp_ids((B)))). + +parse_version_only_test_() -> + [ + ?_assertMatchVersion(20, <<"20~">>), + ?_assertMatchVersion(0, <<"0~">>), + + % Test error clauses + % Make sure that random separators are not recognized + ?_assertErrorVersion(<<"1=">>), + % Alphanumeric character in version + ?_assertErrorVersion(<<"1a~">>), + % Missing separator at the end + ?_assertErrorVersion(<<"1">>), + % Separator at the start + ?_assertErrorVersion(<<"~1">>), + % Separator at both eds + ?_assertErrorVersion(<<"~1~">>) + ]. + +parse_atps_only_test_() -> + [ + % Make sure the order is preserved after parsing + ?_assertMatchAtp([1, 10, 100, 20], atps([1, 10, 100, 20])), + ?_assertMatchAtp([1], atps([1])), + % Empty list of version should NOT cause an error + ?_assertMatchAtp([], atps([])), + + % Test error clauses + % Alphanumeric ids are not allowed + ?_assertErrorAtp(<<"1.1.a">>), + % Trailing separators are not allowed + ?_assertErrorAtp(<<"1.">>), + % Invalid separator + ?_assertErrorAtp(<<"1,2,3">>) + ]. + +parse_test_() -> + [ + ?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = []}}, parse(<<"1~">>)), + ?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = [1]}}, parse(<<"1~1">>)), + ?_assertMatch({ok, #addtl_consent{version = 1, atp_ids = [1, 3, 2]}}, parse(<<"1~1.3.2">>)), + + % Test errors + ?_assertMatch({error, invalid_ac_string}, parse(<<"1~a">>)), + ?_assertMatch({error, invalid_ac_string}, parse(<<"1a1~">>)) + ]. + +%% Helpers +parse(Bin) -> + additional_consent_string:parse(Bin). + +parse_version(Bin) -> + additional_consent_string:parse_version(Bin). + +parse_atp_ids(Bin) -> + additional_consent_string:parse_atp_ids(Bin). + +atps([]) -> + <<"">>; +atps(List) -> + list_to_binary( + lists:join(".", [integer_to_list(X) || X <- List]) + ).