From 422fb08d111068fd8ca35150d7b2c49b48f940b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 31 Jan 2020 17:37:19 +0100 Subject: Update to draft-ietf-httpbis-header-structure-15 --- Makefile | 5 +- src/cow_http_struct_hd.erl | 173 ++++++++++++++++++++++++++++++++------------- 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(<>, Acc, Order, K) when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C) - or (C =:= $_) or (C =:= $-) or (C =:= $*) -> - parse_dict_key(R, Acc, Order, <>). + or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) -> + parse_dict_key(R, Acc, Order, <>); +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(<>, 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(<>, 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(<>, 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(<>, Acc, Order) when ?IS_LC_ALPHA(C) -> parse_dict_key(R, Acc, Order, <>). @@ -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(<>, 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(<>, 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(<>, 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(<>, Acc) when ?IS_WS(C) -> +parse_before_param(<<$\s,R/bits>>, Acc) -> parse_before_param(R, Acc); parse_before_param(<>, Acc) when ?IS_LC_ALPHA(C) -> parse_param(R, Acc, <>). 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(<>, 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, <>); 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(<>) when ?IS_DIGIT(C) -> parse_number(R, 1, <>); %% String. parse_bare_item(<<$",R/bits>>) -> parse_string(R, <<>>); %% Token. -parse_bare_item(<>) when ?IS_ALPHA(C) -> parse_token(R, <>); +parse_bare_item(<>) when ?IS_ALPHA(C) or (C =:= $*) -> parse_token(R, <>); %% 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(<>, L, Acc) when ?IS_DIGIT(C) -> parse_number(R, L+1, <>); -parse_number(<>, L, Acc) when C =:= $. -> - parse_float(R, L, 0, <>); +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(<>, L1, L2, Acc) when ?IS_DIGIT(C) -> - parse_float(R, L1, L2+1, <>); -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(<>, L1, L2, IntAcc, FracAcc) when ?IS_DIGIT(C) -> + parse_decimal(R, L1, L2+1, IntAcc, <>); +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>> -> <<>>; + <> -> <>; + <> -> <>; + <> -> <>; + _ -> 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, <>); @@ -215,7 +237,7 @@ parse_token(<>, 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(<>, Acc) when ?IS_ALPHANUM(C) or (C =:= $+) or (C =:= $/) or (C =:= $=) -> parse_binary(R, <>). @@ -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(<>) 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, <>); escape_string(<<$",R/bits>>, Acc) -> escape_string(R, <>); @@ -386,7 +459,7 @@ escape_string(<>, Acc) -> escape_string(R, <>). params(Params) -> maps:fold(fun - (Key, undefined, Acc) -> + (Key, true, Acc) -> [[$;, Key]|Acc]; (Key, Value, Acc) -> [[$;, Key, $=, bare_item(Value)]|Acc] -- cgit v1.2.3