aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2020-01-31 17:37:19 +0100
committerLoïc Hoguin <[email protected]>2020-01-31 17:37:19 +0100
commit422fb08d111068fd8ca35150d7b2c49b48f940b5 (patch)
treee85c023e024034ec0f9827e00126c72b989c2209
parente7852121a01e511b002e3af2851b7b8780ca1902 (diff)
downloadcowlib-422fb08d111068fd8ca35150d7b2c49b48f940b5.tar.gz
cowlib-422fb08d111068fd8ca35150d7b2c49b48f940b5.tar.bz2
cowlib-422fb08d111068fd8ca35150d7b2c49b48f940b5.zip
Update to draft-ietf-httpbis-header-structure-15
-rw-r--r--Makefile5
-rw-r--r--src/cow_http_struct_hd.erl173
2 files changed, 126 insertions, 52 deletions
diff --git a/Makefile b/Makefile
index 78fa01c..48ef8b5 100644
--- a/Makefile
+++ b/Makefile
@@ -21,11 +21,12 @@ LOCAL_DEPS = crypto
DOC_DEPS = asciideck
TEST_DEPS = $(if $(CI_ERLANG_MK),ci.erlang.mk) base32 horse proper jsx \
- structured-header-tests uritemplate-tests
+ decimal structured-header-tests uritemplate-tests
dep_base32 = git https://github.com/dnsimple/base32_erlang master
dep_horse = git https://github.com/ninenines/horse.git master
dep_jsx = git https://github.com/talentdeficit/jsx v2.10.0
-dep_structured-header-tests = git https://github.com/httpwg/structured-header-tests e614583397e7f65e0082c0fff3929f32a298b9f2
+dep_decimal = git https://github.com/egobrain/decimal 0.4.4
+dep_structured-header-tests = git https://github.com/httpwg/structured-header-tests master
dep_uritemplate-tests = git https://github.com/uri-templates/uritemplate-test master
# CI configuration.
diff --git a/src/cow_http_struct_hd.erl b/src/cow_http_struct_hd.erl
index 373c8da..4138929 100644
--- a/src/cow_http_struct_hd.erl
+++ b/src/cow_http_struct_hd.erl
@@ -18,7 +18,7 @@
%% Dictionary: map()
%% Bare item: one bare_item() that can be of type:
%% Integer: integer()
-%% Float: float()
+%% Decimal: {decimal, {integer(), integer()}}
%% String: {string, binary()}
%% Token: {token, binary()}
%% Byte sequence: {binary, binary()}
@@ -40,11 +40,12 @@
-type sh_list() :: [sh_item() | sh_inner_list()].
-type sh_inner_list() :: sh_with_params([sh_item()]).
--type sh_params() :: #{binary() => sh_bare_item() | undefined}.
+-type sh_params() :: #{binary() => sh_bare_item()}.
-type sh_dictionary() :: {#{binary() => sh_item() | sh_inner_list()}, [binary()]}.
-type sh_item() :: sh_with_params(sh_bare_item()).
--type sh_bare_item() :: integer() | float() | boolean()
+-type sh_bare_item() :: integer() | sh_decimal() | boolean()
| {string | token | binary, binary()}.
+-type sh_decimal() :: {decimal, {integer(), integer()}}.
-type sh_with_params(Type) :: {with_params, Type, sh_params()}.
-define(IS_LC_ALPHA(C),
@@ -75,17 +76,20 @@ parse_dict_key(<<$=,R0/bits>>, Acc, Order, K) ->
parse_dict_before_sep(R, Acc#{K => Item}, [K|Order]);
parse_dict_key(<<C,R/bits>>, Acc, Order, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
- or (C =:= $_) or (C =:= $-) or (C =:= $*) ->
- parse_dict_key(R, Acc, Order, <<K/binary,C>>).
+ or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) ->
+ parse_dict_key(R, Acc, Order, <<K/binary,C>>);
+parse_dict_key(R, Acc, Order, K) ->
+ false = maps:is_key(K, Acc),
+ parse_dict_before_sep(R, Acc#{K => {with_params, true, #{}}}, [K|Order]).
-parse_dict_before_sep(<<C,R/bits>>, Acc, Order) when ?IS_WS(C) ->
+parse_dict_before_sep(<<$\s,R/bits>>, Acc, Order) ->
parse_dict_before_sep(R, Acc, Order);
parse_dict_before_sep(<<C,R/bits>>, Acc, Order) when C =:= $, ->
parse_dict_before_member(R, Acc, Order);
parse_dict_before_sep(<<>>, Acc, Order) ->
{Acc, lists:reverse(Order), <<>>}.
-parse_dict_before_member(<<C,R/bits>>, Acc, Order) when ?IS_WS(C) ->
+parse_dict_before_member(<<$\s,R/bits>>, Acc, Order) ->
parse_dict_before_member(R, Acc, Order);
parse_dict_before_member(<<C,R/bits>>, Acc, Order) when ?IS_LC_ALPHA(C) ->
parse_dict_key(R, Acc, Order, <<C>>).
@@ -117,21 +121,21 @@ parse_list_member(R0, Acc) ->
{Item, R} = parse_item1(R0),
parse_list_before_sep(R, [Item|Acc]).
-parse_list_before_sep(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
+parse_list_before_sep(<<$\s,R/bits>>, Acc) ->
parse_list_before_sep(R, Acc);
parse_list_before_sep(<<$,,R/bits>>, Acc) ->
parse_list_before_member(R, Acc);
parse_list_before_sep(<<>>, Acc) ->
lists:reverse(Acc).
-parse_list_before_member(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
+parse_list_before_member(<<$\s,R/bits>>, Acc) ->
parse_list_before_member(R, Acc);
parse_list_before_member(R, Acc) ->
parse_list_member(R, Acc).
%% Internal.
-parse_inner_list(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
+parse_inner_list(<<$\s,R/bits>>, Acc) ->
parse_inner_list(R, Acc);
parse_inner_list(<<$),$;,R0/bits>>, Acc) ->
{Params, R} = parse_before_param(R0, #{}),
@@ -143,13 +147,14 @@ parse_inner_list(R0, Acc) ->
true = (C =:= $\s) orelse (C =:= $)),
parse_inner_list(R, [Item|Acc]).
-parse_before_param(<<C,R/bits>>, Acc) when ?IS_WS(C) ->
+parse_before_param(<<$\s,R/bits>>, Acc) ->
parse_before_param(R, Acc);
parse_before_param(<<C,R/bits>>, Acc) when ?IS_LC_ALPHA(C) ->
parse_param(R, Acc, <<C>>).
parse_param(<<$;,R/bits>>, Acc, K) ->
- parse_before_param(R, Acc#{K => undefined});
+ false = maps:is_key(K, Acc),
+ parse_before_param(R, Acc#{K => true});
parse_param(<<$=,R0/bits>>, Acc, K) ->
case parse_bare_item(R0) of
{Item, <<$;,R/bits>>} ->
@@ -161,42 +166,59 @@ parse_param(<<$=,R0/bits>>, Acc, K) ->
end;
parse_param(<<C,R/bits>>, Acc, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
- or (C =:= $_) or (C =:= $-) or (C =:= $*) ->
+ or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) ->
parse_param(R, Acc, <<K/binary,C>>);
parse_param(R, Acc, K) ->
false = maps:is_key(K, Acc),
- {Acc#{K => undefined}, R}.
+ {Acc#{K => true}, R}.
-%% Integer or float.
+%% Integer or decimal.
parse_bare_item(<<$-,R/bits>>) -> parse_number(R, 0, <<$->>);
parse_bare_item(<<C,R/bits>>) when ?IS_DIGIT(C) -> parse_number(R, 1, <<C>>);
%% String.
parse_bare_item(<<$",R/bits>>) -> parse_string(R, <<>>);
%% Token.
-parse_bare_item(<<C,R/bits>>) when ?IS_ALPHA(C) -> parse_token(R, <<C>>);
+parse_bare_item(<<C,R/bits>>) when ?IS_ALPHA(C) or (C =:= $*) -> parse_token(R, <<C>>);
%% Byte sequence.
-parse_bare_item(<<$*,R/bits>>) -> parse_binary(R, <<>>);
+parse_bare_item(<<$:,R/bits>>) -> parse_binary(R, <<>>);
%% Boolean.
parse_bare_item(<<"?0",R/bits>>) -> {false, R};
parse_bare_item(<<"?1",R/bits>>) -> {true, R}.
parse_number(<<C,R/bits>>, L, Acc) when ?IS_DIGIT(C) ->
parse_number(R, L+1, <<Acc/binary,C>>);
-parse_number(<<C,R/bits>>, L, Acc) when C =:= $. ->
- parse_float(R, L, 0, <<Acc/binary,C>>);
+parse_number(<<$.,R/bits>>, L, Acc) ->
+ parse_decimal(R, L, 0, Acc, <<>>);
parse_number(R, L, Acc) when L =< 15 ->
{binary_to_integer(Acc), R}.
-parse_float(<<C,R/bits>>, L1, L2, Acc) when ?IS_DIGIT(C) ->
- parse_float(R, L1, L2+1, <<Acc/binary,C>>);
-parse_float(R, L1, L2, Acc) when
- L1 =< 9, L2 =< 6;
- L1 =< 10, L2 =< 5;
- L1 =< 11, L2 =< 4;
- L1 =< 12, L2 =< 3;
- L1 =< 13, L2 =< 2;
- L1 =< 14, L2 =< 1 ->
- {binary_to_float(Acc), R}.
+parse_decimal(<<C,R/bits>>, L1, L2, IntAcc, FracAcc) when ?IS_DIGIT(C) ->
+ parse_decimal(R, L1, L2+1, IntAcc, <<FracAcc/binary,C>>);
+parse_decimal(R, L1, L2, IntAcc, FracAcc0) when L1 =< 12, L2 >= 1, L2 =< 3 ->
+ %% While not strictly required this gives a more consistent representation.
+ FracAcc = case FracAcc0 of
+ <<$0>> -> <<>>;
+ <<$0,$0>> -> <<>>;
+ <<$0,$0,$0>> -> <<>>;
+ <<A,B,$0>> -> <<A,B>>;
+ <<A,$0,$0>> -> <<A>>;
+ <<A,$0>> -> <<A>>;
+ _ -> FracAcc0
+ end,
+ Mul = case byte_size(FracAcc) of
+ 3 -> 1000;
+ 2 -> 100;
+ 1 -> 10;
+ 0 -> 1
+ end,
+ Int = binary_to_integer(IntAcc),
+ Frac = case FracAcc of
+ <<>> -> 0;
+ %% Mind the sign.
+ _ when Int < 0 -> -binary_to_integer(FracAcc);
+ _ -> binary_to_integer(FracAcc)
+ end,
+ {{decimal, {Int * Mul + Frac, -byte_size(FracAcc)}}, R}.
parse_string(<<$\\,$",R/bits>>, Acc) ->
parse_string(R, <<Acc/binary,$">>);
@@ -215,7 +237,7 @@ parse_token(<<C,R/bits>>, Acc) when ?IS_TOKEN(C) or (C =:= $:) or (C =:= $/) ->
parse_token(R, Acc) ->
{{token, Acc}, R}.
-parse_binary(<<$*,R/bits>>, Acc) ->
+parse_binary(<<$:,R/bits>>, Acc) ->
{{binary, base64:decode(Acc)}, R};
parse_binary(<<C,R/bits>>, Acc) when ?IS_ALPHANUM(C) or (C =:= $+) or (C =:= $/) or (C =:= $=) ->
parse_binary(R, <<Acc/binary,C>>).
@@ -293,8 +315,9 @@ e2tb(#{<<"__type">> := <<"binary">>, <<"value">> := V}) ->
{binary, base32:decode(V)};
e2tb(V) when is_binary(V) ->
{string, V};
-e2tb(null) ->
- undefined;
+e2tb(V) when is_float(V) ->
+ %% There should be no rounding needed for the test cases.
+ {decimal, decimal:to_decimal(V, #{precision => 3, rounding => round_down})};
e2tb(V) ->
V.
@@ -308,7 +331,7 @@ e2tb(V) ->
raw_to_binary(RawList) ->
trim_ws(iolist_to_binary(lists:join(<<", ">>, RawList))).
-trim_ws(<<C,R/bits>>) when ?IS_WS(C) -> trim_ws(R);
+trim_ws(<<$\s,R/bits>>) -> trim_ws(R);
trim_ws(R) -> trim_ws_end(R, byte_size(R) - 1).
trim_ws_end(_, -1) ->
@@ -316,7 +339,6 @@ trim_ws_end(_, -1) ->
trim_ws_end(Value, N) ->
case binary:at(Value, N) of
$\s -> trim_ws_end(Value, N - 1);
- $\t -> trim_ws_end(Value, N - 1);
_ ->
S = N + 1,
<< Value2:S/binary, _/bits >> = Value,
@@ -334,7 +356,10 @@ dictionary(Map) when is_map(Map) ->
dictionary(maps:to_list(Map));
dictionary(KVList) when is_list(KVList) ->
lists:join(<<", ">>, [
- [Key, $=, item_or_inner_list(Value)]
+ case Value of
+ true -> Key;
+ _ -> [Key, $=, item_or_inner_list(Value)]
+ end
|| {Key, Value} <- KVList]).
-spec item(sh_item()) -> iolist().
@@ -355,30 +380,78 @@ inner_list({with_params, List, Params}) ->
bare_item({string, String}) ->
[$", escape_string(String, <<>>), $"];
+%% @todo Must fail if Token has invalid characters.
bare_item({token, Token}) ->
Token;
bare_item({binary, Binary}) ->
- [$*, base64:encode(Binary), $*];
+ [$:, base64:encode(Binary), $:];
+bare_item({decimal, {Base, Exp}}) when Exp >= 0 ->
+ Mul = case Exp of
+ 0 -> 1;
+ 1 -> 10;
+ 2 -> 100;
+ 3 -> 1000;
+ 4 -> 10000;
+ 5 -> 100000;
+ 6 -> 1000000;
+ 7 -> 10000000;
+ 8 -> 100000000;
+ 9 -> 1000000000;
+ 10 -> 10000000000;
+ 11 -> 100000000000;
+ 12 -> 1000000000000
+ end,
+ MaxLenWithSign = if
+ Base < 0 -> 13;
+ true -> 12
+ end,
+ Bin = integer_to_binary(Base * Mul),
+ true = byte_size(Bin) =< MaxLenWithSign,
+ [Bin, <<".0">>];
+bare_item({decimal, {Base, -1}}) ->
+ Int = Base div 10,
+ Frac = abs(Base) rem 10,
+ [integer_to_binary(Int), $., integer_to_binary(Frac)];
+bare_item({decimal, {Base, -2}}) ->
+ Int = Base div 100,
+ Frac = abs(Base) rem 100,
+ [integer_to_binary(Int), $., integer_to_binary(Frac)];
+bare_item({decimal, {Base, -3}}) ->
+ Int = Base div 1000,
+ Frac = abs(Base) rem 1000,
+ [integer_to_binary(Int), $., integer_to_binary(Frac)];
+bare_item({decimal, {Base, Exp}}) ->
+ Div = exp_div(Exp),
+ Int0 = Base div Div,
+ true = abs(Int0) < 1000000000000,
+ Frac0 = abs(Base) rem Div,
+ DivFrac = Div div 1000,
+ Frac1 = Frac0 div DivFrac,
+ {Int, Frac} = if
+ (Frac0 rem DivFrac) > (DivFrac div 2) ->
+ case Frac1 of
+ 999 when Int0 < 0 -> {Int0 - 1, 0};
+ 999 -> {Int0 + 1, 0};
+ _ -> {Int0, Frac1 + 1}
+ end;
+ true ->
+ {Int0, Frac1}
+ end,
+ [integer_to_binary(Int), $., if
+ Frac < 10 -> [$0, $0, integer_to_binary(Frac)];
+ Frac < 100 -> [$0, integer_to_binary(Frac)];
+ true -> integer_to_binary(Frac)
+ end];
bare_item(Integer) when is_integer(Integer) ->
integer_to_binary(Integer);
-%% In order to properly reproduce the float as a string we
-%% must first determine how many decimals we want in the
-%% fractional component, otherwise rounding errors may occur.
-bare_item(Float) when is_float(Float) ->
- Decimals = case trunc(Float) of
- I when I >= 10000000000000 -> 1;
- I when I >= 1000000000000 -> 2;
- I when I >= 100000000000 -> 3;
- I when I >= 10000000000 -> 4;
- I when I >= 1000000000 -> 5;
- _ -> 6
- end,
- float_to_binary(Float, [{decimals, Decimals}, compact]);
bare_item(true) ->
<<"?1">>;
bare_item(false) ->
<<"?0">>.
+exp_div(0) -> 1;
+exp_div(N) -> 10 * exp_div(N + 1).
+
escape_string(<<>>, Acc) -> Acc;
escape_string(<<$\\,R/bits>>, Acc) -> escape_string(R, <<Acc/binary,$\\,$\\>>);
escape_string(<<$",R/bits>>, Acc) -> escape_string(R, <<Acc/binary,$\\,$">>);
@@ -386,7 +459,7 @@ escape_string(<<C,R/bits>>, Acc) -> escape_string(R, <<Acc/binary,C>>).
params(Params) ->
maps:fold(fun
- (Key, undefined, Acc) ->
+ (Key, true, Acc) ->
[[$;, Key]|Acc];
(Key, Value, Acc) ->
[[$;, Key, $=, bare_item(Value)]|Acc]