Current section
Files
Jump to
Current section
Files
src/cloudi_service_filesystem_parse.erl
%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==CloudI Filesystem Parsing of HTTP data==
%%% @end
%%%
%%% MIT LICENSE
%%%
%%% Copyright (c) 2011-2014, Loïc Hoguin <essen@ninenines.eu>
%%%
%%% Permission to use, copy, modify, and/or distribute this software for any
%%% purpose with or without fee is hereby granted, provided that the above
%%% copyright notice and this permission notice appear in all copies.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%%%
%%% @author Loïc Hoguin <essen@ninenines.eu>
%%% @copyright 2011-2014 Loïc Hoguin
%%% @version 1.7.1 {@date} {@time}
%%%------------------------------------------------------------------------
-module(cloudi_service_filesystem_parse).
-author('essen [at] ninenines (dot) eu').
%% external interface
-export([datetime/1,
range/1]).
-spec datetime(binary()) ->
calendar:datetime() |
{error, badarg}.
datetime(Binary) ->
http_date(Binary).
-spec range(binary()) ->
{binary(),
list({non_neg_integer(),
non_neg_integer() | infinity} |
neg_integer())} |
{error, badarg}.
range(Binary) ->
token_ci(Binary, fun range/2).
%%%------------------------------------------------------------------------
%%% From cowboy1, in cowboy1_http.erl
%%%------------------------------------------------------------------------
%% @doc Parse an HTTP date (RFC1123, RFC850 or asctime date).
%% @end
%%
%% While this may not be the most efficient date parsing we can do,
%% it should work fine for our purposes because all HTTP dates should
%% be sent as RFC1123 dates in HTTP/1.1.
-spec http_date(binary()) ->
calendar:datetime() | {error, badarg}.
http_date(Data) ->
case rfc1123_date(Data) of
{error, badarg} ->
case rfc850_date(Data) of
{error, badarg} ->
case asctime_date(Data) of
{error, badarg} ->
{error, badarg};
HTTPDate ->
HTTPDate
end;
HTTPDate ->
HTTPDate
end;
HTTPDate ->
HTTPDate
end.
%% @doc Parse an RFC1123 date.
-spec rfc1123_date(binary()) ->
calendar:datetime() | {error, badarg}.
rfc1123_date(Data) ->
wkday(Data,
fun (<< ", ", Rest/binary >>, _WkDay) ->
date1(Rest,
fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
end);
(_Any, _Date) ->
{error, badarg}
end);
(_Any, _WkDay) ->
{error, badarg}
end).
%% @doc Parse an RFC850 date.
-spec rfc850_date(binary()) ->
calendar:datetime() | {error, badarg}.
%% From the RFC:
%% HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date
%% which appears to be more than 50 years in the future is in fact
%% in the past (this helps solve the "year 2000" problem).
rfc850_date(Data) ->
weekday(Data,
fun (<< ", ", Rest/binary >>, _WeekDay) ->
date2(Rest,
fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
end);
(_Any, _Date) ->
{error, badarg}
end);
(_Any, _WeekDay) ->
{error, badarg}
end).
%% @doc Parse an asctime date.
-spec asctime_date(binary()) ->
calendar:datetime() | {error, badarg}.
asctime_date(Data) ->
wkday(Data,
fun (<< " ", Rest/binary >>, _WkDay) ->
date3(Rest,
fun (<< " ", Rest2/binary >>, PartialDate) ->
time(Rest2,
fun (<< " ", Rest3/binary >>, Time) ->
asctime_year(Rest3,
PartialDate, Time);
(_Any, _Time) ->
{error, badarg}
end);
(_Any, _PartialDate) ->
{error, badarg}
end);
(_Any, _WkDay) ->
{error, badarg1}
end).
-spec asctime_year(binary(), tuple(), tuple()) ->
calendar:datetime() | {error, badarg}.
asctime_year(<< Y1, Y2, Y3, Y4, Rest/binary >>, {Month, Day}, Time)
when Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
Year = (Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
http_date_ret(Rest, {{Year, Month, Day}, Time}).
-spec http_date_ret(binary(), tuple()) ->
calendar:datetime() | {error, badarg}.
http_date_ret(Data, DateTime = {Date, _Time}) ->
whitespace(Data,
fun (<<>>) ->
case calendar:valid_date(Date) of
true -> DateTime;
false -> {error, badarg}
end;
(_Any) ->
{error, badarg}
end).
%% We never use it, pretty much just checks the wkday is right.
-spec wkday(binary(), fun()) -> any().
wkday(<< WkDay:3/binary, Rest/binary >>, Fun)
when WkDay =:= <<"Mon">>; WkDay =:= <<"Tue">>; WkDay =:= <<"Wed">>;
WkDay =:= <<"Thu">>; WkDay =:= <<"Fri">>; WkDay =:= <<"Sat">>;
WkDay =:= <<"Sun">> ->
Fun(Rest, WkDay);
wkday(_Any, _Fun) ->
{error, badarg}.
%% We never use it, pretty much just checks the weekday is right.
-spec weekday(binary(), fun()) -> any().
weekday(<< "Monday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Monday">>);
weekday(<< "Tuesday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Tuesday">>);
weekday(<< "Wednesday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Wednesday">>);
weekday(<< "Thursday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Thursday">>);
weekday(<< "Friday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Friday">>);
weekday(<< "Saturday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Saturday">>);
weekday(<< "Sunday", Rest/binary >>, Fun) ->
Fun(Rest, <<"Sunday">>);
weekday(_Any, _Fun) ->
{error, badarg}.
-spec date1(binary(), fun()) -> any().
date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
case month(M) of
{error, badarg} ->
{error, badarg};
Month ->
Fun(Rest, {
(Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
Month,
(D1 - $0) * 10 + (D2 - $0)
})
end;
date1(_Data, _Fun) ->
{error, badarg}.
-spec date2(binary(), fun()) -> any().
date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9 ->
case month(M) of
{error, badarg} ->
{error, badarg};
Month ->
Year = (Y1 - $0) * 10 + (Y2 - $0),
Year2 = case Year > 50 of
true -> Year + 1900;
false -> Year + 2000
end,
Fun(Rest, {
Year2,
Month,
(D1 - $0) * 10 + (D2 - $0)
})
end;
date2(_Data, _Fun) ->
{error, badarg}.
-spec date3(binary(), fun()) -> any().
date3(<< M:3/binary, " ", D1, D2, Rest/binary >>, Fun)
when (D1 >= $0 andalso D1 =< $3) orelse D1 =:= $\s,
D2 >= $0, D2 =< $9 ->
case month(M) of
{error, badarg} ->
{error, badarg};
Month ->
Day = case D1 of
$\s -> D2 - $0;
D1 -> (D1 - $0) * 10 + (D2 - $0)
end,
Fun(Rest, {Month, Day})
end;
date3(_Data, _Fun) ->
{error, badarg}.
-spec month(<< _:24 >>) -> 1..12 | {error, badarg}.
month(<<"Jan">>) -> 1;
month(<<"Feb">>) -> 2;
month(<<"Mar">>) -> 3;
month(<<"Apr">>) -> 4;
month(<<"May">>) -> 5;
month(<<"Jun">>) -> 6;
month(<<"Jul">>) -> 7;
month(<<"Aug">>) -> 8;
month(<<"Sep">>) -> 9;
month(<<"Oct">>) -> 10;
month(<<"Nov">>) -> 11;
month(<<"Dec">>) -> 12;
month(_Any) -> {error, badarg}.
-spec time(binary(), fun()) -> any().
time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/binary >>, Fun)
when H1 >= $0, H1 =< $2, H2 >= $0, H2 =< $9,
M1 >= $0, M1 =< $5, M2 >= $0, M2 =< $9,
S1 >= $0, S1 =< $5, S2 >= $0, S2 =< $9 ->
Hour = (H1 - $0) * 10 + (H2 - $0),
case Hour < 24 of
true ->
Time = {
Hour,
(M1 - $0) * 10 + (M2 - $0),
(S1 - $0) * 10 + (S2 - $0)
},
Fun(Rest, Time);
false ->
{error, badarg}
end.
range(Data, Token) ->
whitespace(Data,
fun(<<"=", Rest/binary>>) ->
case list(Rest, fun range_beginning/2) of
{error, badarg} ->
{error, badarg};
Ranges ->
{Token, Ranges}
end;
(_) ->
{error, badarg}
end).
range_beginning(Data, Fun) ->
range_digits(Data, suffix,
fun(D, RangeBeginning) ->
range_ending(D, Fun, RangeBeginning)
end).
range_ending(Data, Fun, RangeBeginning) ->
whitespace(Data,
fun(<<"-", R/binary>>) ->
case RangeBeginning of
suffix ->
range_digits(R,
fun(D, RangeEnding) ->
Fun(D, -RangeEnding)
end);
_ ->
range_digits(R, infinity,
fun(D, RangeEnding) ->
Fun(D, {RangeBeginning, RangeEnding})
end)
end;
(_) ->
{error, badarg}
end).
-spec range_digits(binary(), fun()) -> any().
range_digits(Data, Fun) ->
whitespace(Data,
fun(D) ->
digits(D, Fun)
end).
-spec range_digits(binary(), any(), fun()) -> any().
range_digits(Data, Default, Fun) ->
whitespace(Data,
fun(<< C, Rest/binary >>) when C >= $0, C =< $9 ->
digits(Rest, Fun, C - $0);
(_) ->
Fun(Data, Default)
end).
-spec list(binary(), fun()) -> list() | {error, badarg}.
list(Data, Fun) ->
case list(Data, Fun, []) of
{error, badarg} -> {error, badarg};
L -> lists:reverse(L)
end.
-spec list(binary(), fun(), [binary()]) -> [any()] | {error, badarg}.
%% From the RFC:
%% <blockquote>Wherever this construct is used, null elements are allowed,
%% but do not contribute to the count of elements present.
%% That is, "(element), , (element) " is permitted, but counts
%% as only two elements. Therefore, where at least one element is required,
%% at least one non-null element MUST be present.</blockquote>
list(Data, Fun, Acc) ->
whitespace(Data,
fun (<<>>) -> Acc;
(<< $,, Rest/binary >>) -> list(Rest, Fun, Acc);
(Rest) -> Fun(Rest,
fun (D, I) -> whitespace(D,
fun (<<>>) -> [I|Acc];
(<< $,, R/binary >>) -> list(R, Fun, [I|Acc]);
(_Any) -> {error, badarg}
end)
end)
end).
-spec digits(binary(), fun()) -> any().
digits(<< C, Rest/binary >>, Fun)
when C >= $0, C =< $9 ->
digits(Rest, Fun, C - $0);
digits(_Data, _Fun) ->
{error, badarg}.
-spec digits(binary(), fun(), non_neg_integer()) -> any().
digits(<< C, Rest/binary >>, Fun, Acc)
when C >= $0, C =< $9 ->
digits(Rest, Fun, Acc * 10 + (C - $0));
digits(Data, Fun, Acc) ->
Fun(Data, Acc).
%% Changes all characters to lowercase.
-spec token_ci(binary(), fun()) -> any().
token_ci(Data, Fun) ->
token(Data, Fun, ci, <<>>).
%-spec token(binary(), fun()) -> any().
%token(Data, Fun) ->
% token(Data, Fun, cs, <<>>).
-spec token(binary(), fun(), ci | cs, binary()) -> any().
token(<<>>, Fun, _Case, Acc) ->
Fun(<<>>, Acc);
token(Data = << C, _Rest/binary >>, Fun, _Case, Acc)
when C =:= $(; C =:= $); C =:= $<; C =:= $>; C =:= $@;
C =:= $,; C =:= $;; C =:= $:; C =:= $\\; C =:= $";
C =:= $/; C =:= $[; C =:= $]; C =:= $?; C =:= $=;
C =:= ${; C =:= $}; C =:= $\s; C =:= $\t;
C < 32; C =:= 127 ->
Fun(Data, Acc);
token(<< C, Rest/binary >>, Fun, Case = ci, Acc) ->
C2 = char_to_lower(C),
token(Rest, Fun, Case, << Acc/binary, C2 >>);
token(<< C, Rest/binary >>, Fun, Case, Acc) ->
token(Rest, Fun, Case, << Acc/binary, C >>).
%% @doc Skip whitespace.
-spec whitespace(binary(), fun()) -> any().
whitespace(<< C, Rest/binary >>, Fun)
when C =:= $\s; C =:= $\t ->
whitespace(Rest, Fun);
whitespace(Data, Fun) ->
Fun(Data).
%% Tests.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
http_date_test_() ->
%% {Tokens, Result}
Tests = [
{<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
{<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
{<<"Sun Nov 6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}}
],
[{V, fun() -> R = http_date(V) end} || {V, R} <- Tests].
rfc1123_date_test_() ->
%% {Tokens, Result}
Tests = [
{<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}
],
[{V, fun() -> R = rfc1123_date(V) end} || {V, R} <- Tests].
rfc850_date_test_() ->
%% {Tokens, Result}
Tests = [
{<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}}
],
[{V, fun() -> R = rfc850_date(V) end} || {V, R} <- Tests].
asctime_date_test_() ->
%% {Tokens, Result}
Tests = [
{<<"Sun Nov 6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}}
],
[{V, fun() -> R = asctime_date(V) end} || {V, R} <- Tests].
http_range_test_() ->
Tests = [
{<<"bytes=1-20">>,
{<<"bytes">>, [{1, 20}]}},
{<<"bytes=-100">>,
{<<"bytes">>, [-100]}},
{<<"bytes=1-">>,
{<<"bytes">>, [{1, infinity}]}},
{<<"bytes=1-20,30-40,50-">>,
{<<"bytes">>, [{1, 20}, {30, 40}, {50, infinity}]}},
{<<"bytes = 1 - 20 , 50 - , - 300 ">>,
{<<"bytes">>, [{1, 20}, {50, infinity}, -300]}},
{<<"bytes=1-20,-500,30-40">>,
{<<"bytes">>, [{1, 20}, -500, {30, 40}]}},
{<<"test=1-20,-500,30-40">>,
{<<"test">>, [{1, 20}, -500, {30, 40}]}},
{<<"bytes=-">>,
{error, badarg}},
{<<"bytes=-30,-">>,
{error, badarg}}
],
[fun() -> R = range(V) end ||{V, R} <- Tests].
-endif.
%%%------------------------------------------------------------------------
%%% From cowboy1, in cowboy1_bstr.erl
%%%------------------------------------------------------------------------
-spec char_to_lower(char()) -> char().
char_to_lower($A) -> $a;
char_to_lower($B) -> $b;
char_to_lower($C) -> $c;
char_to_lower($D) -> $d;
char_to_lower($E) -> $e;
char_to_lower($F) -> $f;
char_to_lower($G) -> $g;
char_to_lower($H) -> $h;
char_to_lower($I) -> $i;
char_to_lower($J) -> $j;
char_to_lower($K) -> $k;
char_to_lower($L) -> $l;
char_to_lower($M) -> $m;
char_to_lower($N) -> $n;
char_to_lower($O) -> $o;
char_to_lower($P) -> $p;
char_to_lower($Q) -> $q;
char_to_lower($R) -> $r;
char_to_lower($S) -> $s;
char_to_lower($T) -> $t;
char_to_lower($U) -> $u;
char_to_lower($V) -> $v;
char_to_lower($W) -> $w;
char_to_lower($X) -> $x;
char_to_lower($Y) -> $y;
char_to_lower($Z) -> $z;
char_to_lower(Ch) -> Ch.