Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions _checkouts/rebar3_purl_gen/src/rebar3_purl_gen.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
-define(TYPE_DIR, "priv/spec/types").

-define(SUPPORTED_SCHEMAS, [
<<"https://packageurl.org/schemas/purl-type-definition.schema-1.0.json">>,
<<"https://packageurl.org/schemas/purl-type.schema-1.0.json">>
<<"https://packageurl.org/schemas/purl-type-definition.schema-1.0.json">>
]).

-spec init(State :: term()) -> {ok, term()}.
Expand Down
35 changes: 27 additions & 8 deletions lib/purl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -195,22 +195,41 @@ defmodule Purl do
@spec to_uri(purl :: t()) :: URI.t()
def to_uri(%Purl{} = purl), do: purl |> Purl.to_record() |> :purl.to_uri() |> then(&struct!(URI, &1))

@typedoc """
Options for `new/2`

* `:strict` - Reject input that is not in canonical form where the
specification requires the canonical form. Defaults to `false`.
Currently this rejects qualifier keys that are not lowercase.
"""
@typedoc since: "0.6.0"
@type parse_option() :: {:strict, boolean()}

@doc """
Creates a new purl struct from a `Purl`, `URI` or string.

See `t:parse_option/0` for the available options.

## Examples

iex> Purl.new("pkg:hex/purl")
{:ok, %Purl{type: "hex", name: "purl"}}

iex> Purl.new("pkg:hex/purl?Key=value")
{:ok, %Purl{type: "hex", name: "purl", qualifiers: %{"key" => "value"}}}

iex> Purl.new("pkg:hex/purl?Key=value", strict: true)
{:error, %Purl.Error.InvalidField{field: :qualifiers, value: "Key"}}

"""
@spec new(purl :: String.t() | URI.t() | t()) :: {:ok, t()} | {:error, parse_error()}
def new(purl)
def new(%Purl{} = purl), do: purl |> to_record() |> :purl.new() |> purl_response()
def new(purl), do: purl |> :purl.new() |> purl_response()
@spec new(purl :: String.t() | URI.t() | t(), options :: [parse_option()]) ::
{:ok, t()} | {:error, parse_error()}
def new(purl, options \\ [])
def new(%Purl{} = purl, options), do: purl |> to_record() |> :purl.new(Map.new(options)) |> purl_response()
def new(purl, options), do: purl |> :purl.new(Map.new(options)) |> purl_response()

@doc """
Similar to `new/1` but raises `URI.Error`, `Purl.Error.InvalidField` or
Similar to `new/2` but raises `URI.Error`, `Purl.Error.InvalidField` or
`Purl.Error.InvalidURI` if an invalid input is given.

## Examples
Expand All @@ -225,9 +244,9 @@ defmodule Purl do
** (Purl.Error.InvalidField) invalid field type, \"hex*\" given

"""
@spec new!(purl :: String.t() | URI.t() | t()) :: t()
def new!(purl) do
case new(purl) do
@spec new!(purl :: String.t() | URI.t() | t(), options :: [parse_option()]) :: t()
def new!(purl, options \\ []) do
case new(purl, options) do
{:ok, purl} -> purl
{:error, reason} -> raise reason
end
Expand Down
2 changes: 1 addition & 1 deletion priv/spec
Submodule spec updated 146 files
43 changes: 39 additions & 4 deletions src/purl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ https://github.com/package-url/purl-spec

-export_type([
parse_error/0,
parse_options/0,
validation_field/0,
type/0,
namespace_segment/0,
Expand All @@ -66,6 +67,7 @@ https://github.com/package-url/purl-spec
from_resource_uri/2,
lookup_type/1,
new/1,
new/2,
register_type/1,
start/2,
stop/1,
Expand All @@ -86,6 +88,16 @@ stop(_State) ->
-type validation_field() ::
namespace | name | version | subpath | qualifiers | {qualifiers, binary()}.

-doc """
Options for `new/2`

* `strict` - Reject input that is not in canonical form where the
specification requires the canonical form. Defaults to `false`.
Currently this rejects qualifier keys that are not lowercase.
""".
-doc #{since => <<"0.6.0">>}.
-type parse_options() :: #{strict => boolean()}.

-doc #{since => <<"0.3.0">>}.
-type parse_error() ::
{error,
Expand Down Expand Up @@ -161,7 +173,8 @@ qualifier key
* The key must be composed only of ASCII letters and numbers, '.', '-' and '_' (period, dash and underscore)
* A key cannot start with a number
* A key must NOT be percent-encoded
* A key is case insensitive. The canonical form is lowercase
* A key must be lowercase. `new/1` lowercases keys, `new/2` with
`strict => true` rejects keys that are not lowercase
* A key cannot contains spaces
""".
-doc #{since => <<"0.3.0">>}.
Expand Down Expand Up @@ -349,8 +362,30 @@ Creates a new purl struct from a `Purl`, `URI` or string.
-spec new(Purl) -> {ok, t()} | parse_error() when
Purl :: uri_string:uri_string() | uri_string:uri_map() | t().
new(Purl) ->
new(Purl, #{}).

-doc """
Creates a new purl struct from a `Purl`, `URI` or string.

See `t:parse_options/0` for the available options.

## Examples

```
> purl:new(<<"pkg:hex/purl?Key=value">>, #{strict => false}).
{ok, #purl{type = <<"hex">>, name = <<"purl">>, namespace = [], subpath = [], qualifiers = #{<<"key">> => <<"value">>}}}
> purl:new(<<"pkg:hex/purl?Key=value">>, #{strict => true}).
{error, {invalid_field, qualifiers, <<"Key">>}}
```

""".
-doc #{since => <<"0.6.0">>}.
-spec new(Purl, Options) -> {ok, t()} | parse_error() when
Purl :: uri_string:uri_string() | uri_string:uri_map() | t(),
Options :: parse_options().
new(Purl, Options) ->
maybe
{ok, #purl{type = Type} = Parsed} ?= purl_parser:parse(Purl),
{ok, #purl{type = Type} = Parsed} ?= purl_parser:parse(Purl, Options),
TypeSpecification = lookup_type(Type),
Normalized = purl_type_normalizer:normalize(Parsed, TypeSpecification),
ok ?= purl_type_validator:validate(Normalized, TypeSpecification),
Expand Down Expand Up @@ -435,12 +470,12 @@ Lookup Type Specification
#{requirement => <<"optional">>,case_sensitive => false,
normalization_rules => [],
note =>
<<"The namespace is optional; it may be used to specify the organization for private packages on hex.pm. It is not case sensitive and must be lowercased.">>,
<<"The namespace is optional; it may be used to specify the organization for private packages on hex.pm. It is not case sensitive and shall be lowercased.">>,
native_name => <<"organization for private packages">>},
name_definition =>
#{requirement => <<"required">>,case_sensitive => false,
normalization_rules => [],
note => <<"The name is not case sensitive and must be lowercased.">>,
note => <<"The name is not case sensitive and shall be lowercased.">>,
native_name => <<"name">>},
version_definition =>
#{requirement => <<"optional">>,case_sensitive => true,
Expand Down
70 changes: 44 additions & 26 deletions src/purl_parser.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,24 @@

-moduledoc false.

-export([parse/1]).
-export([parse/2]).

-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) ->
-spec parse(Purl, Options) -> {ok, purl:t()} | {error, purl:parse_error()} when
Purl :: uri_string:uri_string() | uri_string:uri_map() | purl:t(),
Options :: purl:parse_options().
parse(Purl, Options) when is_binary(Purl) or is_list(Purl) ->
maybe
#{} = UriMap ?= uri_string:parse(Purl),
parse(UriMap)
parse(UriMap, Options)
end;
parse(#{} = UriMap) when not (is_map_key(scheme, UriMap)) ->
parse(#{} = UriMap, _Options) when not (is_map_key(scheme, UriMap)) ->
{error, {invalid_scheme, undefined}};
parse(#{scheme := Scheme} = UriMap) when Scheme =/= <<"pkg">> ->
parse(#{scheme := Scheme} = UriMap, Options) when Scheme =/= <<"pkg">> ->
case string:lowercase(Scheme) of
<<"pkg">> -> parse(maps:put(scheme, <<"pkg">>, UriMap));
<<"pkg">> -> parse(maps:put(scheme, <<"pkg">>, UriMap), Options);
_ -> {error, {invalid_scheme, Scheme}}
end;
parse(#{scheme := <<"pkg">>, host := Type, path := Path} = UriMap) ->
parse(#{scheme := <<"pkg">>, host := Type, path := Path} = UriMap, Options) ->
parse(
maps:remove(
host,
Expand All @@ -31,12 +32,14 @@ parse(#{scheme := <<"pkg">>, host := Type, path := Path} = UriMap) ->
<<Type/binary, "/", Path/binary>>,
UriMap
)
)
),
Options
);
parse(#{scheme := <<"pkg">>, path := Path} = UriMap) ->
parse(#{scheme := <<"pkg">>, path := Path} = UriMap, Options) ->
Strict = maps:get(strict, Options, false),
maybe
{ok, {Type, Namespace, Name, Version}} ?= parse_path(Path),
{ok, Qualifiers} ?= parse_query(maps:get(query, UriMap, undefined)),
{ok, Qualifiers} ?= parse_query(maps:get(query, UriMap, undefined), Strict),
Subpath = parse_fragment(maps:get(fragment, UriMap, undefined)),
Purl = #purl{
type = Type,
Expand All @@ -46,15 +49,16 @@ parse(#{scheme := <<"pkg">>, path := Path} = UriMap) ->
qualifiers = Qualifiers,
subpath = Subpath
},
parse(Purl)
parse(Purl, Options)
end;
parse(#purl{} = Purl) ->
parse(#purl{} = Purl, Options) ->
Strict = maps:get(strict, Options, false),
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, Qualifiers} ?= parse_qualifiers(Purl#purl.qualifiers, Strict),
{ok, Subpath} ?= parse_subpath(Purl#purl.subpath),
{ok, #purl{
type = Type,
Expand Down Expand Up @@ -104,18 +108,23 @@ parse_path(Path) ->
{ok, {Type, Namespace, Name, Version}}
end.

-spec parse_query(Query) ->
-spec parse_query(Query, Strict) ->
{ok, #{unicode:chardata() => unicode:chardata()}} | purl:parse_error()
when
Query :: unicode:chardata() | undefined.
parse_query(undefined) ->
Query :: unicode:chardata() | undefined,
Strict :: boolean().
parse_query(undefined, _Strict) ->
{ok, #{}};
parse_query(Query) ->
parse_query(Query, Strict) ->
Parts = uri_string:dissect_query(Query),
lists:foldl(
fun
({RawKey, Value}, {ok, Acc}) ->
Key = string:lowercase(RawKey),
Key =
case Strict of
true -> RawKey;
false -> string:lowercase(RawKey)
end,
case Acc of
#{Key := _} -> {error, {duplicate_qualifier, Key}};
_ -> {ok, maps:put(Key, Value, Acc)}
Expand Down Expand Up @@ -212,17 +221,18 @@ parse_version(Version) ->
true -> {ok, Version}
end.

-spec parse_qualifiers(RawQualifiers) ->
-spec parse_qualifiers(RawQualifiers, Strict) ->
{ok, Qualifiers} | purl:parse_error()
when
RawQualifiers :: #{unicode:chardata() => unicode:chardata()},
Strict :: boolean(),
Qualifiers :: purl:qualifiers().
parse_qualifiers(RawQualifiers) ->
parse_qualifiers(RawQualifiers, Strict) ->
Result = maps:fold(
fun
(RawQualifierKey, RawQualifierValue, {ok, Acc}) ->
maybe
{ok, QualifierKey} ?= parse_qualifier_key(RawQualifierKey),
{ok, QualifierKey} ?= parse_qualifier_key(RawQualifierKey, Strict),
{ok, QualifierValue} ?= parse_qualifier_value(RawQualifierValue),
{ok, maps:put(QualifierKey, QualifierValue, Acc)}
else
Expand All @@ -240,14 +250,22 @@ parse_qualifiers(RawQualifiers) ->
{error, Reason} -> {error, Reason}
end.

-spec parse_qualifier_key(RawQualifierKey) ->
-spec parse_qualifier_key(RawQualifierKey, Strict) ->
{ok, QualifierKey} | {error, {invalid_field, qualifiers, QualifierKey}}
when
Strict :: boolean(),
RawQualifierKey :: unicode:chardata(),
QualifierKey :: purl:qualifier_key().
parse_qualifier_key(<<"">>) ->
parse_qualifier_key(<<"">>, _Strict) ->
{error, {invalid_field, qualifiers, <<"">>}};
parse_qualifier_key(QualifierKey) ->
parse_qualifier_key(QualifierKey, true) ->
case re:run(QualifierKey, "^[a-z\\.\\-\\_][a-z0-9\\.\\-\\_]*$") of
{match, _} ->
{ok, QualifierKey};
nomatch ->
{error, {invalid_field, qualifiers, QualifierKey}}
end;
parse_qualifier_key(QualifierKey, false) ->
case re:run(QualifierKey, "^[a-zA-Z\\.\\-\\_][a-zA-Z0-9\\.\\-\\_]*$") of
{match, _} ->
{ok, QualifierKey};
Expand Down
Loading
Loading