blob: c3513dced37cc98b52c2632ee73ee1ccd3511185 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(mango_util).
-export([
open_doc/2,
open_ddocs/1,
load_ddoc/2,
defer/3,
do_defer/3,
assert_ejson/1,
to_lower/1,
enc_dbname/1,
dec_dbname/1,
enc_hex/1,
dec_hex/1,
lucene_escape_field/1,
lucene_escape_query_value/1,
lucene_escape_user/1,
is_number_string/1,
has_suffix/2,
join/2,
parse_field/1,
cached_re/2
]).
-include_lib("couch/include/couch_db.hrl").
-include("mango.hrl").
-define(DIGITS, "(\\p{N}+)").
-define(HEXDIGITS, "([0-9a-fA-F]+)").
-define(EXP, "[eE][+-]?" ++ ?DIGITS).
-define(NUMSTRING,
"[\\x00-\\x20]*" ++ "[+-]?(" ++ "NaN|"
++ "Infinity|" ++ "((("
++ ?DIGITS
++ "(\\.)?("
++ ?DIGITS
++ "?)("
++ ?EXP
++ ")?)|"
++ "(\\.("
++ ?DIGITS
++ ")("
++ ?EXP
++ ")?)|"
++ "(("
++ "(0[xX]"
++ ?HEXDIGITS
++ "(\\.)?)|"
++ "(0[xX]"
++ ?HEXDIGITS
++ "?(\\.)"
++ ?HEXDIGITS
++ ")"
++ ")[pP][+-]?" ++ ?DIGITS ++ "))" ++ "[fFdD]?))" ++ "[\\x00-\\x20]*").
open_doc(Db, DocId) ->
open_doc(Db, DocId, [deleted, ejson_body]).
open_doc(Db, DocId, Options) ->
case mango_util:defer(fabric, open_doc, [Db, DocId, Options]) of
{ok, Doc} ->
{ok, Doc};
{not_found, _} ->
not_found;
_ ->
?MANGO_ERROR({error_loading_doc, DocId})
end.
open_ddocs(Db) ->
case mango_util:defer(fabric, design_docs, [Db]) of
{ok, Docs} ->
{ok, Docs};
_ ->
?MANGO_ERROR(error_loading_ddocs)
end.
load_ddoc(Db, DDocId) ->
case open_doc(Db, DDocId, [deleted, ejson_body]) of
{ok, Doc} ->
{ok, check_lang(Doc)};
not_found ->
Body = {[
{<<"language">>, <<"query">>}
]},
{ok, #doc{id = DDocId, body = Body}}
end.
defer(Mod, Fun, Args) ->
{Pid, Ref} = erlang:spawn_monitor(?MODULE, do_defer, [Mod, Fun, Args]),
receive
{'DOWN', Ref, process, Pid, {mango_defer_ok, Value}} ->
Value;
{'DOWN', Ref, process, Pid, {mango_defer_throw, Value}} ->
erlang:throw(Value);
{'DOWN', Ref, process, Pid, {mango_defer_error, Value}} ->
erlang:error(Value);
{'DOWN', Ref, process, Pid, {mango_defer_exit, Value}} ->
erlang:exit(Value)
end.
do_defer(Mod, Fun, Args) ->
try erlang:apply(Mod, Fun, Args) of
Resp ->
erlang:exit({mango_defer_ok, Resp})
catch
throw:Error ->
Stack = erlang:get_stacktrace(),
couch_log:error("Defered error: ~w~n ~p", [{throw, Error}, Stack]),
erlang:exit({mango_defer_throw, Error});
error:Error ->
Stack = erlang:get_stacktrace(),
couch_log:error("Defered error: ~w~n ~p", [{error, Error}, Stack]),
erlang:exit({mango_defer_error, Error});
exit:Error ->
Stack = erlang:get_stacktrace(),
couch_log:error("Defered error: ~w~n ~p", [{exit, Error}, Stack]),
erlang:exit({mango_defer_exit, Error})
end.
assert_ejson({Props}) ->
assert_ejson_obj(Props);
assert_ejson(Vals) when is_list(Vals) ->
assert_ejson_arr(Vals);
assert_ejson(null) ->
true;
assert_ejson(true) ->
true;
assert_ejson(false) ->
true;
assert_ejson(String) when is_binary(String) ->
true;
assert_ejson(Number) when is_number(Number) ->
true;
assert_ejson(_Else) ->
false.
assert_ejson_obj([]) ->
true;
assert_ejson_obj([{Key, Val} | Rest]) when is_binary(Key) ->
case assert_ejson(Val) of
true ->
assert_ejson_obj(Rest);
false ->
false
end;
assert_ejson_obj(_Else) ->
false.
assert_ejson_arr([]) ->
true;
assert_ejson_arr([Val | Rest]) ->
case assert_ejson(Val) of
true ->
assert_ejson_arr(Rest);
false ->
false
end.
check_lang(#doc{id = Id, deleted = true}) ->
Body = {[
{<<"language">>, <<"query">>}
]},
#doc{id = Id, body = Body};
check_lang(#doc{body = {Props}} = Doc) ->
case lists:keyfind(<<"language">>, 1, Props) of
{<<"language">>, <<"query">>} ->
Doc;
Else ->
?MANGO_ERROR({invalid_ddoc_lang, Else})
end.
to_lower(Key) when is_binary(Key) ->
KStr = binary_to_list(Key),
KLower = string:to_lower(KStr),
list_to_binary(KLower).
enc_dbname(<<>>) ->
<<>>;
enc_dbname(<<A:8/integer, Rest/binary>>) ->
Bytes = enc_db_byte(A),
Tail = enc_dbname(Rest),
<<Bytes/binary, Tail/binary>>.
enc_db_byte(N) when N >= $a, N =< $z -> <<N>>;
enc_db_byte(N) when N >= $0, N =< $9 -> <<N>>;
enc_db_byte(N) when N == $/; N == $_; N == $- -> <<N>>;
enc_db_byte(N) ->
H = enc_hex_byte(N div 16),
L = enc_hex_byte(N rem 16),
<<$$, H:8/integer, L:8/integer>>.
dec_dbname(<<>>) ->
<<>>;
dec_dbname(<<$$, _:8/integer>>) ->
throw(invalid_dbname_encoding);
dec_dbname(<<$$, H:8/integer, L:8/integer, Rest/binary>>) ->
Byte = (dec_hex_byte(H) bsl 4) bor dec_hex_byte(L),
Tail = dec_dbname(Rest),
<<Byte:8/integer, Tail/binary>>;
dec_dbname(<<N:8/integer, Rest/binary>>) ->
Tail = dec_dbname(Rest),
<<N:8/integer, Tail/binary>>.
enc_hex(<<>>) ->
<<>>;
enc_hex(<<V:8/integer, Rest/binary>>) ->
H = enc_hex_byte(V div 16),
L = enc_hex_byte(V rem 16),
Tail = enc_hex(Rest),
<<H:8/integer, L:8/integer, Tail/binary>>.
enc_hex_byte(N) when N >= 0, N < 10 -> $0 + N;
enc_hex_byte(N) when N >= 10, N < 16 -> $a + (N - 10);
enc_hex_byte(N) -> throw({invalid_hex_value, N}).
dec_hex(<<>>) ->
<<>>;
dec_hex(<<_:8/integer>>) ->
throw(invalid_hex_string);
dec_hex(<<H:8/integer, L:8/integer, Rest/binary>>) ->
Byte = (dec_hex_byte(H) bsl 4) bor dec_hex_byte(L),
Tail = dec_hex(Rest),
<<Byte:8/integer, Tail/binary>>.
dec_hex_byte(N) when N >= $0, N =< $9 -> (N - $0);
dec_hex_byte(N) when N >= $a, N =< $f -> (N - $a) + 10;
dec_hex_byte(N) when N >= $A, N =< $F -> (N - $A) + 10;
dec_hex_byte(N) -> throw({invalid_hex_character, N}).
lucene_escape_field(Bin) when is_binary(Bin) ->
Str = binary_to_list(Bin),
Enc = lucene_escape_field(Str),
iolist_to_binary(Enc);
lucene_escape_field([H | T]) when is_number(H), H >= 0, H =< 255 ->
if
H >= $a, $z >= H ->
[H | lucene_escape_field(T)];
H >= $A, $Z >= H ->
[H | lucene_escape_field(T)];
H >= $0, $9 >= H ->
[H | lucene_escape_field(T)];
true ->
Hi = enc_hex_byte(H div 16),
Lo = enc_hex_byte(H rem 16),
[$_, Hi, Lo | lucene_escape_field(T)]
end;
lucene_escape_field([]) ->
[].
lucene_escape_query_value(IoList) when is_list(IoList) ->
lucene_escape_query_value(iolist_to_binary(IoList));
lucene_escape_query_value(Bin) when is_binary(Bin) ->
IoList = lucene_escape_qv(Bin),
iolist_to_binary(IoList).
% This escapes the special Lucene query characters
% listed below as well as any whitespace.
%
% + - && || ! ( ) { } [ ] ^ ~ * ? : \ " /
%
lucene_escape_qv(<<>>) -> [];
lucene_escape_qv(<<"&&", Rest/binary>>) ->
["\\&&" | lucene_escape_qv(Rest)];
lucene_escape_qv(<<"||", Rest/binary>>) ->
["\\||" | lucene_escape_qv(Rest)];
lucene_escape_qv(<<C, Rest/binary>>) ->
NeedsEscape = "+-(){}[]!^~*?:/\\\" \t\r\n",
Out = case lists:member(C, NeedsEscape) of
true -> ["\\", C];
false -> [C]
end,
Out ++ lucene_escape_qv(Rest).
lucene_escape_user(Field) ->
{ok, Path} = parse_field(Field),
Escaped = [mango_util:lucene_escape_field(P) || P <- Path],
iolist_to_binary(join(".", Escaped)).
has_suffix(Bin, Suffix) when is_binary(Bin), is_binary(Suffix) ->
SBin = size(Bin),
SSuffix = size(Suffix),
if SBin < SSuffix -> false; true ->
PSize = SBin - SSuffix,
case Bin of
<<_:PSize/binary, Suffix/binary>> ->
true;
_ ->
false
end
end.
join(_Sep, [Item]) ->
[Item];
join(Sep, [Item | Rest]) ->
[Item, Sep | join(Sep, Rest)].
is_number_string(Value) when is_binary(Value) ->
is_number_string(binary_to_list(Value));
is_number_string(Value) when is_list(Value)->
MP = cached_re(mango_numstring_re, ?NUMSTRING),
case re:run(Value, MP) of
nomatch ->
false;
_ ->
true
end.
cached_re(Name, RE) ->
case mochiglobal:get(Name) of
undefined ->
{ok, MP} = re:compile(RE),
ok = mochiglobal:put(Name, MP),
MP;
MP ->
MP
end.
parse_field(Field) ->
case binary:match(Field, <<"\\">>, []) of
nomatch ->
% Fast path, no regex required
{ok, check_non_empty(Field, binary:split(Field, <<".">>, [global]))};
_ ->
parse_field_slow(Field)
end.
parse_field_slow(Field) ->
Path = lists:map(fun
(P) when P =:= <<>> ->
?MANGO_ERROR({invalid_field_name, Field});
(P) ->
re:replace(P, <<"\\\\">>, <<>>, [global, {return, binary}])
end, re:split(Field, <<"(?<!\\\\)\\.">>)),
{ok, Path}.
check_non_empty(Field, Parts) ->
case lists:member(<<>>, Parts) of
true ->
?MANGO_ERROR({invalid_field_name, Field});
false ->
Parts
end.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
parse_field_test() ->
?assertEqual({ok, [<<"ab">>]}, parse_field(<<"ab">>)),
?assertEqual({ok, [<<"a">>, <<"b">>]}, parse_field(<<"a.b">>)),
?assertEqual({ok, [<<"a.b">>]}, parse_field(<<"a\\.b">>)),
?assertEqual({ok, [<<"a">>, <<"b">>, <<"c">>]}, parse_field(<<"a.b.c">>)),
?assertEqual({ok, [<<"a">>, <<"b.c">>]}, parse_field(<<"a.b\\.c">>)),
Exception = {mango_error, ?MODULE, {invalid_field_name, <<"a..b">>}},
?assertThrow(Exception, parse_field(<<"a..b">>)).
is_number_string_test() ->
?assert(is_number_string("0")),
?assert(is_number_string("1")),
?assert(is_number_string("1.0")),
?assert(is_number_string("1.0E10")),
?assert(is_number_string("0d")),
?assert(is_number_string("-1")),
?assert(is_number_string("-1.0")),
?assertNot(is_number_string("hello")),
?assertNot(is_number_string("")),
?assertMatch({match, _}, re:run("1.0", mochiglobal:get(mango_numstring_re))).
-endif.