Current section
Files
Jump to
Current section
Files
src/purl_parser.erl
-module(purl_parser).
-feature(maybe_expr, enable).
-include("purl.hrl").
-moduledoc false.
-export([parse/1]).
-spec parse(Purl) -> {ok, purl:t()} | {error, purl:parse_error()} when
Purl :: uri_string:uri_string() | uri_string:uri_map() | purl:t().
parse(Purl) when is_binary(Purl) or is_list(Purl) ->
maybe
#{} = UriMap ?= uri_string:parse(Purl),
parse(UriMap)
end;
parse(#{} = UriMap) when not (is_map_key(scheme, UriMap)) ->
{error, {invalid_scheme, undefined}};
parse(#{scheme := Scheme} = UriMap) when Scheme =/= <<"pkg">> ->
case string:lowercase(Scheme) of
<<"pkg">> -> parse(maps:put(scheme, <<"pkg">>, UriMap));
_ -> {error, {invalid_scheme, Scheme}}
end;
parse(#{scheme := <<"pkg">>, host := Type, path := Path} = UriMap) ->
parse(
maps:remove(
host,
maps:put(
path,
<<Type/binary, "/", Path/binary>>,
UriMap
)
)
);
parse(#{scheme := <<"pkg">>, path := Path} = UriMap) ->
maybe
{ok, {Type, Namespace, Name, Version}} ?= parse_path(Path),
{ok, Qualifiers} ?= parse_query(maps:get(query, UriMap, undefined)),
Subpath = parse_fragment(maps:get(fragment, UriMap, undefined)),
Purl = #purl{
type = Type,
namespace = Namespace,
name = Name,
version = Version,
qualifiers = Qualifiers,
subpath = Subpath
},
parse(Purl)
end;
parse(#purl{} = Purl) ->
maybe
{ok, Type} ?= parse_type(Purl#purl.type),
{ok, Namespace} ?= parse_namespace(Purl#purl.namespace),
{ok, Name} ?= parse_name(Purl#purl.name),
{ok, Version} ?= parse_version(Purl#purl.version),
{ok, Qualifiers} ?= parse_qualifiers(Purl#purl.qualifiers),
{ok, Subpath} ?= parse_subpath(Purl#purl.subpath),
{ok, #purl{
type = Type,
namespace = Namespace,
name = Name,
version = Version,
qualifiers = Qualifiers,
subpath = Subpath
}}
end.
-spec parse_path(Path) -> {ok, {Type, Namespace, Name, Version}} | purl:parse_error() when
Path :: unicode:chardata(),
Type :: unicode:chardata(),
Namespace :: [unicode:chardata()],
Name :: unicode:chardata(),
Version :: unicode:chardata() | undefined.
parse_path(Path) ->
case binary:split(Path, <<"/">>, [trim_all, global]) of
[_One] ->
{error, {invalid_field, name, <<"">>}};
[RawType | [_NameOrNamespace | _Rest] = Rest] ->
Type = string:lowercase(RawType),
{RawNamespace, [NameWithVersion]} = lists:split(length(Rest) - 1, Rest),
Namespace = lists:map(fun uri_string:percent_decode/1, RawNamespace),
NameWithVersionParts = binary:split(NameWithVersion, <<"@">>),
{Name, Version} =
case NameWithVersionParts of
[RawName] ->
{uri_string:percent_decode(RawName), undefined};
[RawName, RawVersion] ->
{uri_string:percent_decode(RawName), uri_string:percent_decode(RawVersion)}
end,
{ok, {Type, Namespace, Name, Version}}
end.
-spec parse_query(Query) ->
{ok, #{unicode:chardata() => unicode:chardata()}} | purl:parse_error()
when
Query :: unicode:chardata() | undefined.
parse_query(undefined) ->
{ok, #{}};
parse_query(Query) ->
Parts = uri_string:dissect_query(Query),
lists:foldl(
fun
({RawKey, Value}, {ok, Acc}) ->
Key = string:lowercase(RawKey),
case Acc of
#{Key := _} -> {error, {duplicate_qualifier, Key}};
_ -> {ok, maps:put(Key, Value, Acc)}
end;
(_, {error, Reason}) ->
{error, Reason}
end,
{ok, #{}},
Parts
).
-spec parse_fragment(Fragment) -> [unicode:chardata()] when
Fragment :: unicode:chardata() | undefined.
parse_fragment(undefined) ->
[];
parse_fragment(Fragment) ->
Decoded = uri_string:percent_decode(Fragment),
Trimmed = string:trim(Decoded, both, "/"),
binary:split(Trimmed, <<"/">>, [trim_all, global]).
-spec parse_type(RawType) -> {ok, Type} | purl:parse_error() when
RawType :: unicode:chardata(),
Type :: purl:type().
parse_type(Type) ->
case re:run(Type, "^[a-zA-Z\\+\\.\\-][a-zA-Z0-9\\+\\.\\-]*$") of
{match, _} ->
{ok, Type};
nomatch ->
{error, {invalid_field, type, Type}}
end.
-spec parse_namespace(RawNamespace) -> {ok, Namespace} | purl:parse_error() when
RawNamespace :: [unicode:chardata()],
Namespace :: purl:namespace().
parse_namespace(RawNamespace) ->
Result = lists:foldl(
fun
(Segment, {ok, Acc}) ->
case parse_namespace_segment(Segment) of
{ok, Segment} -> {ok, [Segment | Acc]};
skip -> {ok, Acc};
{error, Reason} -> {error, Reason}
end;
(_, {error, Reason}) ->
{error, Reason}
end,
{ok, []},
RawNamespace
),
case Result of
{ok, Namespace} -> {ok, lists:reverse(Namespace)};
{error, Reason} -> {error, Reason}
end.
-spec parse_namespace_segment(RawSegment) ->
{ok, Segment} | skip | {error, {invalid_field, namespace, Segment}}
when
RawSegment :: unicode:chardata(),
Segment :: purl:namespace_segment().
parse_namespace_segment(<<"">>) ->
skip;
parse_namespace_segment(Segment) ->
case valid_utf8(Segment) of
false ->
{error, {invalid_field, namespace, Segment}};
true ->
case binary:match(Segment, <<"/">>) of
{_, _} -> {error, {invalid_field, namespace, Segment}};
nomatch -> {ok, Segment}
end
end.
-spec parse_name(RawName) -> {ok, Name} | purl:parse_error() when
RawName :: unicode:chardata(),
Name :: purl:name().
parse_name(<<"">>) ->
{error, {invalid_field, name, <<"">>}};
parse_name(Name) ->
case valid_utf8(Name) of
false -> {error, {invalid_field, name, Name}};
true -> {ok, Name}
end.
-spec parse_version(RawVersion) ->
{ok, Version} | purl:parse_error()
when
RawVersion :: unicode:chardata(),
Version :: purl:version().
parse_version(undefined) ->
{ok, undefined};
parse_version(Version) ->
case valid_utf8(Version) of
false -> {error, {invalid_field, version, Version}};
true -> {ok, Version}
end.
-spec parse_qualifiers(RawQualifiers) ->
{ok, Qualifiers} | purl:parse_error()
when
RawQualifiers :: #{unicode:chardata() => unicode:chardata()},
Qualifiers :: purl:qualifiers().
parse_qualifiers(RawQualifiers) ->
Result = maps:fold(
fun
(RawQualifierKey, RawQualifierValue, {ok, Acc}) ->
maybe
{ok, QualifierKey} ?= parse_qualifier_key(RawQualifierKey),
{ok, QualifierValue} ?= parse_qualifier_value(RawQualifierValue),
{ok, maps:put(QualifierKey, QualifierValue, Acc)}
else
skip -> {ok, Acc};
{error, Reason} -> {error, Reason}
end;
(_, _, {error, Reason}) ->
{error, Reason}
end,
{ok, #{}},
RawQualifiers
),
case Result of
{ok, Qualifiers} -> {ok, Qualifiers};
{error, Reason} -> {error, Reason}
end.
-spec parse_qualifier_key(RawQualifierKey) ->
{ok, QualifierKey} | {error, {invalid_field, qualifiers, QualifierKey}}
when
RawQualifierKey :: unicode:chardata(),
QualifierKey :: purl:qualifier_key().
parse_qualifier_key(<<"">>) ->
{error, {invalid_field, qualifiers, <<"">>}};
parse_qualifier_key(QualifierKey) ->
case re:run(QualifierKey, "^[a-zA-Z\\.\\-\\_][a-zA-Z0-9\\.\\-\\_]*$") of
{match, _} ->
{ok, QualifierKey};
nomatch ->
{error, {invalid_field, qualifiers, QualifierKey}}
end.
-spec parse_qualifier_value(RawQualifierValue) ->
{ok, QualifierValue} | skip | {error, {invalid_field, qualifiers, QualifierValue}}
when
RawQualifierValue :: unicode:chardata(),
QualifierValue :: purl:qualifier_value().
parse_qualifier_value(<<"">>) ->
skip;
parse_qualifier_value(QualifierValue) ->
case valid_utf8(QualifierValue) of
false -> {error, {invalid_field, qualifiers, QualifierValue}};
true -> {ok, QualifierValue}
end.
-spec parse_subpath(RawSubpath) -> {ok, Subpath} | purl:parse_error() when
RawSubpath :: [unicode:chardata()],
Subpath :: purl:subpath().
parse_subpath(RawSubpath) ->
Result = lists:foldl(
fun
(Segment, {ok, Acc}) ->
case parse_subpath_segment(Segment) of
{ok, Segment} -> {ok, [Segment | Acc]};
skip -> {ok, Acc};
{error, Reason} -> {error, Reason}
end;
(_, {error, Reason}) ->
{error, Reason}
end,
{ok, []},
RawSubpath
),
case Result of
{ok, Subpath} -> {ok, lists:reverse(Subpath)};
{error, Reason} -> {error, Reason}
end.
-spec parse_subpath_segment(RawSegment) ->
{ok, Segment} | skip | {error, {invalid_field, subpath, Segment}}
when
RawSegment :: unicode:chardata(),
Segment :: purl:subpath_segment().
parse_subpath_segment(<<"">>) ->
skip;
parse_subpath_segment(<<".">>) ->
skip;
parse_subpath_segment(<<"..">>) ->
skip;
parse_subpath_segment(Segment) ->
case valid_utf8(Segment) of
false -> {error, {invalid_field, subpath, Segment}};
true -> {ok, Segment}
end.
-spec valid_utf8(Binary) -> boolean() when
Binary :: binary().
valid_utf8(<<>>) -> true;
valid_utf8(<<_H/utf8, T/binary>>) -> valid_utf8(T);
valid_utf8(_Binary) -> false.