Browse Source

Add cow_http_hd:parse_content_range/1

From RFC7233.
Loïc Hoguin 10 years ago
parent
commit
c85ef5cf4f
2 changed files with 114 additions and 0 deletions
  1. 4 0
      include/cow_inline.hrl
  2. 110 0
      src/cow_http_hd.erl

+ 4 - 0
include/cow_inline.hrl

@@ -32,6 +32,10 @@
 	C =:= $Z
 ).
 
+%% IS_CHAR(Character)
+
+-define(IS_CHAR(C), C > 0, C < 128).
+
 %% IS_DIGIT(Character)
 
 -define(IS_DIGIT(C),

+ 110 - 0
src/cow_http_hd.erl

@@ -26,6 +26,7 @@
 -export([parse_content_encoding/1]).
 -export([parse_content_language/1]).
 -export([parse_content_length/1]).
+-export([parse_content_range/1]).
 -export([parse_content_type/1]).
 -export([parse_date/1]).
 -export([parse_etag/1]).
@@ -94,6 +95,9 @@ token() ->
 		non_empty(list(tchar())),
 		list_to_binary(T)).
 
+abnf_char() ->
+	int(1, 127).
+
 vchar() ->
 	int(33, 126).
 
@@ -1434,6 +1438,112 @@ horse_parse_content_length_giga() ->
 	).
 -endif.
 
+%% @doc Parse the Content-Range header.
+
+-spec parse_content_range(binary())
+	-> {bytes, non_neg_integer(), non_neg_integer(), non_neg_integer() | '*'}
+	| {bytes, '*', non_neg_integer()} | {binary(), binary()}.
+parse_content_range(<<"bytes */", C, R/bits >>) when ?IS_DIGIT(C) -> unsatisfied_range(R, C - $0);
+parse_content_range(<<"bytes ", C, R/bits >>) when ?IS_DIGIT(C) -> byte_range_first(R, C - $0);
+parse_content_range(<< C, R/bits >>) when ?IS_TOKEN(C) ->
+	case C of
+		?INLINE_LOWERCASE(other_content_range_unit, R, <<>>)
+	end.
+
+byte_range_first(<< $-, C, R/bits >>, First) when ?IS_DIGIT(C) -> byte_range_last(R, First, C - $0);
+byte_range_first(<< C, R/bits >>, First) when ?IS_DIGIT(C) -> byte_range_first(R, First * 10 + C - $0).
+
+byte_range_last(<<"/*">>, First, Last) -> {bytes, First, Last, '*'};
+byte_range_last(<< $/, C, R/bits >>, First, Last) when ?IS_DIGIT(C) -> byte_range_complete(R, First, Last, C - $0);
+byte_range_last(<< C, R/bits >>, First, Last) when ?IS_DIGIT(C) -> byte_range_last(R, First, Last * 10 + C - $0).
+
+byte_range_complete(<<>>, First, Last, Complete) -> {bytes, First, Last, Complete};
+byte_range_complete(<< C, R/bits >>, First, Last, Complete) when ?IS_DIGIT(C) ->
+	byte_range_complete(R, First, Last, Complete * 10 + C - $0).
+
+unsatisfied_range(<<>>, Complete) -> {bytes, '*', Complete};
+unsatisfied_range(<< C, R/bits >>, Complete) when ?IS_DIGIT(C) -> unsatisfied_range(R, Complete * 10 + C - $0).
+
+other_content_range_unit(<< $\s, R/bits >>, Unit) -> other_content_range_resp(R, Unit, <<>>);
+other_content_range_unit(<< C, R/bits >>, Unit) when ?IS_TOKEN(C) ->
+	case C of
+		?INLINE_LOWERCASE(other_content_range_unit, R, Unit)
+	end.
+
+other_content_range_resp(<<>>, Unit, Resp) -> {Unit, Resp};
+other_content_range_resp(<< C, R/bits >>, Unit, Resp) when ?IS_CHAR(C) -> other_content_range_resp(R, Unit, << Resp/binary, C >>).
+
+-ifdef(TEST).
+content_range() ->
+	?LET(ContentRange,
+		oneof([
+			?SUCHTHAT({bytes, First, Last, Complete},
+				{bytes, non_neg_integer(), non_neg_integer(), non_neg_integer()},
+				First =< Last andalso Last < Complete),
+			?SUCHTHAT({bytes, First, Last, '*'},
+				{bytes, non_neg_integer(), non_neg_integer(), '*'},
+				First =< Last),
+			{bytes, '*', non_neg_integer()},
+			{token(), ?LET(L, list(abnf_char()), list_to_binary(L))}
+		]),
+		{case ContentRange of
+			{Unit, Resp} when is_binary(Unit) -> {?INLINE_LOWERCASE_BC(Unit), Resp};
+			_ -> ContentRange
+		end, case ContentRange of
+			{bytes, First, Last, '*'} ->
+				<< "bytes ", (integer_to_binary(First))/binary, "-",
+					(integer_to_binary(Last))/binary, "/*">>;
+			{bytes, First, Last, Complete} ->
+				<< "bytes ", (integer_to_binary(First))/binary, "-",
+					(integer_to_binary(Last))/binary, "/", (integer_to_binary(Complete))/binary >>;
+			{bytes, '*', Complete} ->
+				<< "bytes */", (integer_to_binary(Complete))/binary >>;
+			{Unit, Resp} ->
+				<< Unit/binary, $\s, Resp/binary >>
+		end}).
+
+prop_parse_content_range() ->
+	?FORALL({Res, ContentRange},
+		content_range(),
+		Res =:= parse_content_range(ContentRange)).
+
+parse_content_range_test_() ->
+	Tests = [
+		{<<"bytes 21010-47021/47022">>, {bytes, 21010, 47021, 47022}},
+		{<<"bytes 500-999/8000">>, {bytes, 500, 999, 8000}},
+		{<<"bytes 7000-7999/8000">>, {bytes, 7000, 7999, 8000}},
+		{<<"bytes 42-1233/1234">>, {bytes, 42, 1233, 1234}},
+		{<<"bytes 42-1233/*">>, {bytes, 42, 1233, '*'}},
+		{<<"bytes */1234">>, {bytes, '*', 1234}},
+		{<<"bytes 0-499/1234">>, {bytes, 0, 499, 1234}},
+		{<<"bytes 500-999/1234">>, {bytes, 500, 999, 1234}},
+		{<<"bytes 500-1233/1234">>, {bytes, 500, 1233, 1234}},
+		{<<"bytes 734-1233/1234">>, {bytes, 734, 1233, 1234}},
+		{<<"bytes */47022">>, {bytes, '*', 47022}},
+		{<<"exampleunit 1.2-4.3/25">>, {<<"exampleunit">>, <<"1.2-4.3/25">>}},
+		{<<"exampleunit 11.2-14.3/25">>, {<<"exampleunit">>, <<"11.2-14.3/25">>}}
+	],
+	[{V, fun() -> R = parse_content_range(V) end} || {V, R} <- Tests].
+
+parse_content_range_error_test_() ->
+	Tests = [
+		<<>>
+	],
+	[{V, fun() -> {'EXIT', _} = (catch parse_content_range(V)) end} || V <- Tests].
+-endif.
+
+-ifdef(PERF).
+horse_parse_content_range_bytes() ->
+	horse:repeat(200000,
+		parse_content_range(<<"bytes 21010-47021/47022">>)
+	).
+
+horse_parse_content_range_other() ->
+	horse:repeat(200000,
+		parse_content_range(<<"exampleunit 11.2-14.3/25">>)
+	).
+-endif.
+
 %% @doc Parse the Content-Type header.
 
 -spec parse_content_type(binary()) -> media_type().