Current section

Files

Jump to
purl src purl.erl
Raw

src/purl.erl

-module(purl).
-feature(maybe_expr, enable).
-include("purl.hrl").
-moduledoc """
Erlang Implementation of the purl (package url) specification.
## Specification
https://github.com/package-url/purl-spec
**Format**: `pkg:type/namespace/name@version?qualifiers#subpath`
> #### License {: .neutral}
>
> A lot of the documentation was taken directly from the specification. It is
> licensed under the MIT License:
> ```
> Copyright (c) the purl authors
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of
> this software and associated documentation files (the "Software"), to deal in
> the Software without restriction, including without limitation the rights to
> use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
> the Software, and to permit persons to whom the Software is furnished to do so,
> subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
> FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
> COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
> IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
> CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
> ```
""".
-moduledoc #{since => <<"0.3.0">>}.
-behaviour(application).
-export_type([
parse_error/0,
validation_field/0,
type/0,
namespace_segment/0,
namespace/0,
name/0,
version/0,
qualifier_key/0,
qualifier_value/0,
qualifiers/0,
subpath_segment/0,
subpath/0,
type_component_definition/0,
type_specification/0,
t/0
]).
-export([
equal/2,
from_resource_uri/1,
from_resource_uri/2,
lookup_type/1,
new/1,
register_type/1,
start/2,
stop/1,
to_binary/1,
to_uri/1,
unregister_type/1
]).
-doc false.
start(_StartType, _StartArgs) ->
purl_sup:start_link().
-doc false.
stop(_State) ->
ok.
-doc #{since => <<"0.4.0">>}.
-type validation_field() ::
namespace | name | version | subpath | qualifiers | {qualifiers, binary()}.
-doc #{since => <<"0.3.0">>}.
-type parse_error() ::
{error,
{invalid_field, Field :: atom(), Value :: binary()}
| {duplicate_qualifier, Key :: binary()}
| {invalid_scheme, Scheme :: binary() | undefined}
| {type_validation, Type :: type(), Field :: validation_field(), Value :: binary(),
Reason :: binary()}}
| uri_string:error().
-doc """
the package "type" or package "protocol" such as `maven`, `npm`, `nuget`,
`gem`, `pypi`, etc.
Known types: https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
## Validation
* The package type is composed only of ASCII letters and numbers, '.', '+' and '-' (period, plus, and dash)
* The type cannot start with a number
* The type cannot contains spaces
* The type must NOT be percent-encoded
* The type is case insensitive. The canonical form is lowercase
""".
-doc #{since => <<"0.3.0">>}.
-type type() :: binary().
-doc """
Segment of the namespace
## Validation
* must not contain a '/'
* must not be empty
* A URL host or Authority must NOT be used as a namespace. Use instead a
`repository_url` qualifier. Note however that for some types, the namespace
may look like a host.
""".
-doc #{since => <<"0.3.0">>}.
-type namespace_segment() :: binary().
-doc """
some name prefix such as a Maven groupid, a Docker image owner, a GitHub user
or organization
The values are type-specific.
""".
-doc #{since => <<"0.3.0">>}.
-type namespace() :: [namespace_segment()].
-doc """
the name of the package
""".
-doc #{since => <<"0.3.0">>}.
-type name() :: binary().
-doc """
the version of the package
A version is a plain and opaque string. Some package types use versioning
conventions such as semver for NPMs or nevra conventions for RPMS. A type may
define a procedure to compare and sort versions, but there is no reliable and
uniform way to do such comparison consistently.
""".
-doc #{since => <<"0.3.0">>}.
-type version() :: binary().
-doc """
qualifier key
## Validation
* 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 cannot contains spaces
""".
-doc #{since => <<"0.3.0">>}.
-type qualifier_key() :: binary().
-doc """
qualifier value
## Validation
* value cannot be an empty string: a key=value pair with an empty value is the
same as no key/value at all for this key
""".
-doc #{since => <<"0.3.0">>}.
-type qualifier_value() :: binary().
-doc """
extra qualifying data for a package such as an OS, architecture, a distro,
etc.
The values are type-specific.
## Validation
* key must be unique within the keys of the qualifiers string
""".
-doc #{since => <<"0.3.0">>}.
-type qualifiers() :: #{qualifier_key() => qualifier_value()}.
-doc """
subpath segment
## Validation
* must not contain a '/'
* must not be any of '..' or '.'
* must not be empty
""".
-doc #{since => <<"0.3.0">>}.
-type subpath_segment() :: binary().
-doc """
extra subpath within a package, relative to the package root
""".
-doc #{since => <<"0.3.0">>}.
-type subpath() :: [subpath_segment()].
-doc """
type specification
See: https://github.com/package-url/purl-spec/blob/main/schemas/purl-type-definition.schema.json
## Example
```erlang
#{
'$schema' => <<"https://packageurl.org/schemas/purl-type-definition.schema-1.0.json">>,
'$id' => <<"https://packageurl.org/types/hex-definition.json">>,
type => <<"hex">>,
type_name => <<"Hex">>
%% ...
}
```
""".
-type type_specification() :: #{
'$schema' := binary(),
'$id' := binary(),
type := type(),
type_name := binary(),
description := binary(),
repository := #{
use_repository := boolean(),
default_repository_url => binary(),
note => binary()
},
namespace_definition := #{
requirement := binary(),
permitted_characters => binary(),
case_sensitive => boolean(),
normalization_rules => [binary()],
native_name => binary(),
note => binary()
},
name_definition := #{
requirement := binary(),
permitted_characters => binary(),
case_sensitive => boolean(),
normalization_rules => [binary()],
native_name => binary(),
note => binary()
},
version_definition => #{
requirement := binary(),
permitted_characters => binary(),
case_sensitive => boolean(),
normalization_rules => [binary()],
native_name => binary(),
note => binary()
},
qualifiers_definition => [
#{
key := qualifier_key(),
requirement => binary(),
description := binary(),
default_value => qualifier_value(),
native_name => binary()
}
],
subpath_definition => #{
requirement := binary(),
permitted_characters => binary(),
case_sensitive => boolean(),
normalization_rules => [binary()],
native_name => binary(),
note => binary()
},
examples := [binary()],
note => binary(),
reference_urls => [binary()]
}.
-doc false.
-type type_component_definition() :: #{
permitted_characters => binary(),
case_sensitive => boolean(),
normalization_rules => [binary()],
native_name => binary(),
note => binary(),
%% Ignore Rest
atom() => term()
}.
-doc """
Package URL record
""".
-doc #{since => <<"0.3.0">>}.
-type t() :: #purl{
type :: purl:type(),
namespace :: purl:namespace(),
name :: purl:name(),
version :: purl:version() | undefined,
qualifiers :: purl:qualifiers(),
subpath :: purl:subpath()
}.
-doc """
Formats purl as binary
## Examples
```
> purl:to_binary(#purl{type = "hex", name = "purl", namespace = [], subpath = [], qualifiers = #{}})
<<"pkg:hex/purl">>
```
""".
-doc #{since => <<"0.3.0">>}.
-spec to_binary(Purl) -> unicode:chardata() when Purl :: t().
to_binary(#purl{} = Purl) -> uri_string:recompose(to_uri(Purl)).
-doc """
Converts a purl to a `uri_string:uri_map()`
## Examples
```
> purl:to_uri(#purl{type = "hex", name = "purl", namespace = [], subpath = [], qualifiers = #{}})
#{scheme=><<"pkg">>,path=><<"hex/purl">>}
```
""".
-doc #{since => <<"0.3.0">>}.
-spec to_uri(Purl) -> uri_string:uri_map() when Purl :: t().
to_uri(#purl{} = Purl) -> purl_composer:compose_uri(Purl).
-doc """
Creates a new purl struct from a `Purl`, `URI` or string.
## Examples
```
> purl:new(<<"pkg:hex/purl">>)
{ok, #purl{type = <<"hex">>, name = <<"purl">>, namespace = [], subpath = [], qualifiers = #{}}}
```
""".
-doc #{since => <<"0.3.0">>}.
-spec new(Purl) -> {ok, t()} | parse_error() when
Purl :: uri_string:uri_string() | uri_string:uri_map() | t().
new(Purl) ->
maybe
{ok, #purl{type = Type} = Parsed} ?= purl_parser:parse(Purl),
TypeSpecification = lookup_type(Type),
Normalized = purl_type_normalizer:normalize(Parsed, TypeSpecification),
ok ?= purl_type_validator:validate(Normalized, TypeSpecification),
{ok, Normalized}
end.
-doc """
Convert known URLs to purl
## Currently Supported
* GitHub: Repository HTTP / Git URL, Project URL
* BitBucket: Repository HTTTP / Git URL, Project URL
* Hex.pm package URL
""".
-doc #{since => <<"0.3.0">>}.
-spec from_resource_uri(Uri, FallbackVersion) -> {ok, t()} | error when
Uri :: uri_string:uri_map() | uri_string:uri_string(),
FallbackVersion :: undefined | binary().
from_resource_uri(Uri, FallbackVersion) -> purl_resource:from_uri(Uri, FallbackVersion).
-doc """
Convert known URLs to purl
See `from_resource_uri/2`.
""".
-doc #{since => <<"0.3.0">>}.
-spec from_resource_uri(Uri) -> {ok, t()} | error when
Uri :: uri_string:uri_map() | uri_string:uri_string().
from_resource_uri(Uri) -> from_resource_uri(Uri, undefined).
-doc """
Register Custom PURL Type
## Examples
```
> purl:register_type(
#{
'$schema' => <<"https://packageurl.org/schemas/purl-type-definition.schema-1.0.json">>,
'$id' => <<"https://acme.com/type.json">>,
type => <<"acme-package">>,
type_name => <<"Acme Package">>,
description => <<"Acme Package Type">>,
repository => #{use_repository => false},
namespace_definition => #{requirement => <<"optional">>},
examples => [<<"pkg:acme/acme-package@1.0.0">>]
}
).
ok
```
""".
-doc #{since => <<"0.4.0">>}.
-spec register_type(Specification :: type_specification()) -> ok.
register_type(Specification) -> purl_type_registry:add(Specification).
-doc """
Unregister Custom PURL Type
## Examples
```> purl:unregister_type(<<"acme-package">>).
ok
```
""".
-doc #{since => <<"0.4.0">>}.
-spec unregister_type(Type :: type()) -> ok.
unregister_type(Type) -> purl_type_registry:delete(Type).
-doc """
Lookup Type Specification
## Examples
```
> purl:lookup_type(<<"hex">>).
#{type => <<"hex">>,description => <<"Hex packages">>,
repository =>
#{default_repository_url => <<"https://repo.hex.pm">>,
use_repository => true},
namespace_definition =>
#{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.">>,
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.">>,
native_name => <<"name">>},
version_definition =>
#{requirement => <<"optional">>,case_sensitive => true,
normalization_rules => [],native_name => <<"version">>},
subpath_definition =>
#{requirement => <<"optional">>,case_sensitive => true,
normalization_rules => []},
qualifiers_definition => [],
'$schema' =>
<<"https://packageurl.org/schemas/purl-type-definition.schema-1.0.json">>,
'$id' => <<"https://packageurl.org/types/hex-definition.json">>,
type_name => <<"Hex">>,
examples =>
[<<"pkg:hex/jason@1.1.2">>,<<"pkg:hex/acme/foo@2.3.">>,
<<"pkg:hex/phoenix_html@2.13.3#priv/static/phoenix_html.js">>,
<<"pkg:hex/bar@1.2.3", 63, "repository_url=https://myrepo.example.com">>],
reference_urls => []}
```
""".
-doc #{since => <<"0.4.0">>}.
-spec lookup_type(Type :: type()) -> type_specification().
lookup_type(Type) -> purl_type_registry:lookup(Type).
-doc false.
-spec equal(Purl1 :: t(), Purl2 :: t()) -> boolean().
equal(Purl, Purl) ->
true;
equal(#purl{type = Type1}, #purl{type = Type2}) when Type1 =/= Type2 ->
false;
equal(#purl{namespace = Ns1}, #purl{namespace = Ns2}) when Ns1 =/= Ns2 ->
false;
equal(#purl{name = Name1}, #purl{name = Name2}) when Name1 =/= Name2 ->
false;
equal(#purl{version = Ver1}, #purl{version = Ver2}) when Ver1 =/= Ver2 ->
false;
equal(#purl{subpath = Sub1}, #purl{subpath = Sub2}) when Sub1 =/= Sub2 ->
false;
equal(#purl{type = Type, qualifiers = Q1}, #purl{qualifiers = Q2}) ->
Specification = lookup_type(Type),
qualifiers_equal(Q1, Q2, Specification).
-spec qualifiers_equal(
Q1 :: qualifiers(), Q2 :: qualifiers(), Specification :: type_specification()
) -> boolean().
qualifiers_equal(Q1, Q2, Specification) ->
DefaultQualifiers = purl_type_normalizer:default_qualifiers(Specification),
AllKeys = lists:usort(maps:keys(Q1) ++ maps:keys(Q2) ++ maps:keys(DefaultQualifiers)),
lists:all(
fun(Key) ->
V1 = maps:get(Key, Q1, maps:get(Key, DefaultQualifiers, undefined)),
V2 = maps:get(Key, Q2, maps:get(Key, DefaultQualifiers, undefined)),
values_match(V1, V2)
end,
AllKeys
).
-spec values_match(Value1 :: binary() | undefined, Value2 :: binary() | undefined) -> boolean().
values_match(Value, Value) ->
true;
values_match(undefined, _) ->
false;
values_match(_, undefined) ->
false;
values_match(_, _) ->
false.