%%%---------------------------------------------------------------------- %%% File : fast_yaml.erl %%% Author : Evgeniy Khramtsov %%% Purpose : YAML parser %%% Created : 7 Aug 2013 by Evgeniy Khramtsov %%% %%% %%% Copyright (C) 2002-2019 ProcessOne, SARL. All Rights Reserved. %%% %%% 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(fast_yaml). -compile(no_native). %% API -export([load_nif/0, decode/1, decode/2, start/0, stop/0, decode_from_file/1, decode_from_file/2, encode/1, format_error/1]). -type option() :: {plain_as_atom, boolean()} | plain_as_atom. -type options() :: [option()]. -type parser_error() :: {parser_error, binary(), integer(), integer()}. -type scanner_error() :: {scanner_error, binary(), integer(), integer()}. -type yaml_error() :: parser_error() | scanner_error() | memory_error | unexpected_error. -define(PLAIN_AS_ATOM, 1). %%%=================================================================== %%% API %%%=================================================================== start() -> application:start(fast_yaml). stop() -> application:stop(fast_yaml). load_nif() -> SOPath = p1_nif_utils:get_so_path(?MODULE, [fast_yaml], "fast_yaml"), load_nif(SOPath). load_nif(SOPath) -> case catch erlang:load_nif(SOPath, 0) of ok -> ok; Err -> error_logger:warning_msg("unable to load fast_yaml NIF: ~p~n", [Err]), Err end. -spec format_error(atom() | yaml_error() | file:posix()) -> string(). format_error({Tag, Reason, Line, Column}) when Tag == parser_error; Tag == scanner_error -> lists:flatten( io_lib:format( "Syntax error on line ~p at position ~p: ~s", [Line+1, Column+1, Reason])); format_error(memory_error) -> "Memory error"; format_error(Reason) when is_atom(Reason) -> case file:format_error(Reason) of "unknown POSIX error" -> atom_to_list(Reason); Res -> Res end; format_error(_) -> "Unexpected error". -spec decode(iodata()) -> {ok, term()} | {error, yaml_error()}. decode(Data) -> decode(Data, []). -spec decode_from_file(file:filename()) -> {ok, term()} | {error, yaml_error() | file:posix()}. decode_from_file(File) -> decode_from_file(File, []). -spec decode_from_file(file:filename(), options()) -> {ok, term()} | {error, yaml_error() | file:posix()}. decode_from_file(File, Opts) -> case file:read_file(File) of {ok, Data} -> decode(Data, Opts); Err -> Err end. -spec decode(iodata(), options()) -> {ok, term()} | {error, yaml_error()}. decode(Data, Opts) -> try nif_decode(Data, make_flags(Opts)) catch error:{parser_error, _, _, _} = Reason -> {error, Reason} end. -spec encode(term()) -> iolist(). encode(Term) -> NL = io_lib:nl(), case encode(Term, 0) of [[NL|T1]|T2] -> [T1|T2]; T -> T end. encode([{_, _}|_] = Terms, N) -> [[io_lib:nl(), indent(N), encode_pair(T, N)] || T <- Terms]; encode([_|_] = Terms, N) -> [[io_lib:nl(), indent(N), "- ", encode(T, N+2)] || T <- Terms]; encode([], _) -> "[]"; encode(I, _) when is_integer(I) -> integer_to_list(I); encode(F, _) when is_float(F) -> io_lib:format("~f", [F]); encode(A, _) when is_atom(A) -> atom_to_list(A); % http://erlang.org/doc/reference_manual/data_types.html#escape-sequences encode(B, _) when is_binary(B) -> [$", lists:map( fun ($\b) -> [$\\, "b"]; % $\b == "backspace" %($\d) -> [$\\, "d"]; % $\d = "delete" % Encode work, but decode fail ($\e) -> [$\\, "e"]; % $\e == "escape" ($\f) -> [$\\, "f"]; % $\f == "from feed" ($\n) -> [$\\, "n"]; % $\n == "new line" ($\r) -> [$\\, "r"]; % $\r == "carriage return" ($\s) -> [$\s]; % $\s == "space" ($\t) -> [$\\, "t"]; % $\t == "tab" ($\v) -> [$\\, "v"]; % $\v == "vertical tab" ($") -> [$\\, $"]; % $" == double quote ($\\) -> [$\\, $\\]; % $\\ == backslash (C) -> C end, unicode:characters_to_list(B)), $"]. encode_pair({K, V}, N) -> [encode(K), ": ", encode(V, N+2)]. %%%=================================================================== %%% Internal functions %%%=================================================================== make_flags([{plain_as_atom, true}|Opts]) -> ?PLAIN_AS_ATOM bor make_flags(Opts); make_flags([{plain_as_atom, false}|Opts]) -> make_flags(Opts); make_flags([plain_as_atom|Opts]) -> ?PLAIN_AS_ATOM bor make_flags(Opts); make_flags([Opt|Opts]) -> error_logger:warning_msg("fast_yaml: unknown option ~p", [Opt]), make_flags(Opts); make_flags([]) -> 0. nif_decode(_Data, _Flags) -> error_logger:error_msg("fast_yaml NIF not loaded", []), erlang:nif_error(nif_not_loaded). indent(N) -> lists:duplicate(N, $ ). %%%=================================================================== %%% Unit tests %%%=================================================================== -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). load_nif_test() -> SOPath = p1_nif_utils:get_so_path(?MODULE, [], "fast_yaml"), ?assertEqual(ok, load_nif(SOPath)). decode_test1_test() -> FileName = filename:join(["..", "test", "test1.yml"]), ?assertEqual( {ok,[[{<<"Time">>,<<"2001-11-23 15:01:42 -5">>}, {<<"User">>,<<"ed">>}, {<<"Warning">>, <<"This is an error message for the log file">>}], [{<<"Time">>,<<"2001-11-23 15:02:31 -5">>}, {<<"User">>,<<"ed">>}, {<<"Warning">>,<<"A slightly different error message.">>}], [{<<"Date">>,<<"2001-11-23 15:03:17 -5">>}, {<<"User">>,<<"ed">>}, {<<"Fatal">>,<<"Unknown variable \"bar\"">>}, {<<"Stack">>, [[{<<"file">>,<<"TopClass.py">>}, {<<"line">>,23}, {<<"code">>,<<"x = MoreObject(\"345\\n\")\n">>}], [{<<"file">>,<<"MoreClass.py">>}, {<<"line">>,58}, {<<"code">>,<<"foo = bar">>}]]}]]}, decode_from_file(FileName)). decode_test2_test() -> FileName = filename:join(["..", "test", "test2.yml"]), ?assertEqual( {ok,[[[{step,[{instrument,<<"Lasik 2000">>}, {pulseEnergy,5.4}, {pulseDuration,12}, {repetition,1000}, {spotSize,<<"1mm">>}]}], [{step,[{instrument,<<"Lasik 2000">>}, {pulseEnergy,5.0}, {pulseDuration,10}, {repetition,500}, {spotSize,<<"2mm">>}]}], [{step,<<"id001">>}], [{step,<<"id002">>}], [{step,<<"id001">>}], [{step,<<"id002">>}]]]}, decode_from_file(FileName, [plain_as_atom])). decode_test3_test() -> FileName = filename:join(["..", "test", "test3.yml"]), ?assertEqual( {ok,[[{<<"a">>,123}, {<<"b">>,<<"123">>}, {<<"c">>,123.0}, {<<"d">>,123}, {<<"e">>,123}, {<<"f">>,<<"Yes">>}, {<<"g">>,<<"Yes">>}, {<<"h">>,<<"Yes we have No bananas">>}]]}, decode_from_file(FileName)). decode_test4_test() -> FileName = filename:join(["..", "test", "test4.yml"]), ?assertEqual( {ok,[[{<<"picture">>, <<"R0lGODlhDAAMAIQAAP//9/X\n17unp5WZmZgAAAOfn515eXv\n" "Pz7Y6OjuDg4J+fn5OTk6enp\n56enmleECcgggoBADs=mZmE\n">>}]]}, decode_from_file(FileName)). decode_test5_test() -> FileName = filename:join(["..", "test", "test5.yml"]), ?assertEqual( {ok,[[ {<<"Name">>,<<"Backslash">>}, {<<"Source">>,<<"\\\\\\\\">>}], [{<<"Name">>,<<"Double_Quote">>}, {<<"Source">>,<<"\"\"">>}], [{<<"Name">>,<<"Backslash_and_Double_Quote">>}, {<<"Source">>,<<"\"\\\"\"">>}], [{<<"Name">>,<<"New_Line">>}, {<<"Source">>,<<"\\n">>}]] }, decode_from_file(FileName)). encode_test1_test() -> ?assertEqual( list_to_binary(encode(<<"a">>)), <<"\"a\"">>). encode_unicode_test1_test() -> ?assertEqual( unicode:characters_to_binary(encode(<<"☃"/utf8>>)), <<"\"☃\""/utf8>>). encode_decode_backspace_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"abc\bdef">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_escape_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\en">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_from_feed_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\f\"">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_new_line_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\n">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_carriage_return_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"ref\r\n">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_space_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<" toto\stata \s ">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_tab_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\treturn True">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_vertical_tab_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\v ok">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_simple_quote_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"'\"'">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_double_quote_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\"\"">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_backslash_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\\\\\\\\">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). encode_decode_quote_and_backslash_test() -> FileName = filename:join(["..", "test", "temp_test.yml"]), Binary = <<"\"\\\"\"">>, Encoded = encode([[{'Source', Binary}]]), file:write_file(FileName, Encoded), Decoded = decode_from_file(FileName, [plain_as_atom]), file:delete(FileName), {ok, [[[{'Source', DecodedBinary}]]]} = Decoded, ?assertEqual( DecodedBinary, Binary ). -endif.