%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2016. 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. %% %% %CopyrightEnd% %% -module(diameter_dict_scanner). %% %% A scanner for dictionary files of the form expected by yecc. %% -export([scan/1, format_error/1]). -export([is_name/1]). %% ----------------------------------------------------------- %% # scan/1 %% ----------------------------------------------------------- -spec scan(string() | binary()) -> {ok, [Token]} | {error, {string(), string(), Lineno}} when Token :: {word, Lineno, string()} | {number, Lineno, non_neg_integer()} | {Symbol, Lineno}, Lineno :: pos_integer(), Symbol :: '{' | '}' | '<' | '>' | '[' | ']' | '*' | '::=' | ':' | ',' | '-' | avp_types | avp_vendor_id | codecs | custom_types | define | grouped | id | inherits | messages | name | prefix | vendor | '$end' | code | 'answer-message' | 'AVP' | 'AVP-Header' | 'Diameter' | 'Diameter-Header' | 'Header' | 'REQ' | 'PXY' | 'ERR'. scan(B) when is_binary(B) -> scan(binary_to_list(B)); scan(S) -> scan(S, {1, []}). scan(S, {Lineno, Acc}) -> case split(S) of '$end' = E -> {ok, lists:reverse([{E, Lineno} | Acc])}; {Tok, Rest} -> scan(Rest, acc(Tok, Lineno, Acc)); Reason when is_list(Reason) -> {error, {Reason, S, Lineno}} end. %% format_error/1 format_error({Reason, Input, Lineno}) -> io_lib:format("~s at line ~p: ~s", [Reason, Lineno, head(Input, [], 20, true)]). %% is_name/1 is_name([H|T]) -> is_alphanum(H) andalso lists:all(fun is_name_ch/1, T). %% =========================================================================== head(Str, Acc, N, _) when [] == Str; 0 == N; $\r == hd(Str); $\n == hd(Str) -> lists:reverse(Acc); head([C|Rest], Acc, N, true = T) %% skip leading whitespace when C == $\s; C == $\t; C == $\f; C == $\v -> head(Rest, Acc, N, T); head([C|Rest], Acc, N, _) -> head(Rest, [C|Acc], N-1, false). acc(endline, Lineno, Acc) -> {Lineno + 1, Acc}; acc(T, Lineno, Acc) -> {Lineno, [tok(T, Lineno) | Acc]}. tok({Cat, Sym}, Lineno) -> {Cat, Lineno, Sym}; tok(Sym, Lineno) -> {Sym, Lineno}. %% # split/1 %% %% Output: {Token, Rest} | atom() %% Finito. split("") -> '$end'; %% Skip comments. This precludes using semicolon for any other purpose. split([$;|T]) -> split(lists:dropwhile(fun(C) -> not is_eol_ch(C) end, T)); %% Beginning of a section. split([$@|T]) -> {Name, Rest} = lists:splitwith(fun is_name_ch/1, T), case section(Name) of false -> "Unknown section"; 'end' -> '$end'; A -> {A, Rest} end; split("::=" ++ T) -> {'::=', T}; split([H|T]) when H == ${; H == $}; H == $<; H == $>; H == $[; H == $]; H == $*; H == $:; H == $,; H == $- -> {list_to_atom([H]), T}; %% RFC 3588 requires various names to begin with a letter but 3GPP (for %% one) abuses this. (eg 3GPP-Charging-Id in TS32.299.) split([H|_] = L) when $0 =< H, H =< $9 -> {P, Rest} = splitwith(fun is_name_ch/1, L), Tok = try {number, read_int(P)} catch error:_ -> word(P) end, {Tok, Rest}; split([H|_] = L) when $a =< H, H =< $z; $A =< H, H =< $Z -> {P, Rest} = splitwith(fun is_name_ch/1, L), {word(P), Rest}; split([$'|T]) -> case lists:splitwith(fun(C) -> not lists:member(C, "'\r\n") end, T) of {[_|_] = A, [$'|Rest]} -> {{word, A}, Rest}; {[], [$'|_]} -> "Empty string"; _ -> %% not terminated on same line "Unterminated string" end; %% Line ending of various forms. split([$\r,$\n|T]) -> {endline, T}; split([C|T]) when C == $\r; C == $\n -> {endline, T}; %% Ignore whitespace. split([C|T]) when C == $\s; C == $\t; C == $\f; C == $\v -> split(T); split(_) -> "Unexpected character". %% word/1 %% Reserved words significant in parsing ... word(S) when S == "answer-message"; S == "code"; S == "AVP"; S == "AVP-Header"; S == "Diameter"; S == "Diameter-Header"; S == "Header"; S == "REQ"; S == "PXY"; S == "ERR" -> list_to_atom(S); %% ... or not. word(S) -> {word, S}. %% section/1 section(N) when N == "avp_types"; N == "avp_vendor_id"; N == "codecs"; N == "custom_types"; N == "define"; N == "end"; N == "enum"; N == "grouped"; N == "id"; N == "inherits"; N == "messages"; N == "name"; N == "prefix"; N == "vendor" -> list_to_atom(N); section(_) -> false. %% read_int/1 read_int([$0,X|S]) when X == $X; X == $x -> {ok, [N], []} = io_lib:fread("~16u", S), N; read_int(S) -> list_to_integer(S). %% splitwith/3 splitwith(Fun, [H|T]) -> {SH, ST} = lists:splitwith(Fun, T), {[H|SH], ST}. is_eol_ch(C) -> C == $\n orelse C == $\r. is_name_ch(C) -> is_alphanum(C) orelse C == $- orelse C == $_. is_alphanum(C) -> is_lower(C) orelse is_upper(C) orelse is_digit(C). is_lower(C) -> $a =< C andalso C =< $z. is_upper(C) -> $A =< C andalso C =< $Z. is_digit(C) -> $0 =< C andalso C =< $9.