-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
893 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[submodule "test/spec"] | ||
path = test/spec | ||
url = [email protected]:package-url/purl-spec.git |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,242 @@ | ||
defmodule Purl do | ||
@moduledoc """ | ||
Documentation for `Purl`. | ||
Elixir 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. | ||
> ``` | ||
""" | ||
|
||
alias Purl.Composer | ||
alias Purl.Parser | ||
alias Purl.SpecialCase | ||
|
||
# credo:disable-for-next-line Credo.Check.Warning.SpecWithStruct | ||
@type parse_error :: | ||
%URI.Error{} | ||
| Purl.Error.InvalidField.t() | ||
| Purl.Error.DuplicateQualifier.t() | ||
| Purl.Error.InvalidScheme.t() | ||
|
||
@typedoc """ | ||
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 | ||
""" | ||
@type type :: String.t() | ||
|
||
@typedoc """ | ||
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. | ||
""" | ||
@type namespace_segment :: String.t() | ||
|
||
@typedoc """ | ||
some name prefix such as a Maven groupid, a Docker image owner, a GitHub user | ||
or organization | ||
The values are type-specific. | ||
""" | ||
@type namespace :: [namespace_segment()] | ||
|
||
@typedoc """ | ||
the name of the package | ||
""" | ||
@type name :: String.t() | ||
|
||
@typedoc """ | ||
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. | ||
""" | ||
@type version :: Version.t() | String.t() | ||
|
||
@typedoc """ | ||
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 | ||
""" | ||
@type qualifier_key :: String.t() | ||
|
||
@typedoc """ | ||
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 | ||
""" | ||
@type qualifier_value :: String.t() | ||
|
||
@typedoc """ | ||
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 | ||
""" | ||
@type qualifiers :: %{optional(qualifier_key()) => qualifier_value()} | ||
|
||
@typedoc """ | ||
subpath segment | ||
## Validation | ||
* must not contain a '/' | ||
* must not be any of '..' or '.' | ||
* must not be empty | ||
""" | ||
@type subpath_segment :: String.t() | ||
|
||
@typedoc """ | ||
extra subpath within a package, relative to the package root | ||
""" | ||
@type subpath :: [subpath_segment()] | ||
|
||
@typedoc """ | ||
Package URL struct | ||
""" | ||
@type t :: %__MODULE__{ | ||
type: type(), | ||
namespace: namespace(), | ||
name: name(), | ||
version: version() | nil, | ||
qualifiers: qualifiers(), | ||
subpath: subpath() | ||
} | ||
|
||
@enforce_keys [:type, :name] | ||
defstruct [:type, :name, namespace: [], version: nil, qualifiers: %{}, subpath: []] | ||
|
||
@doc """ | ||
Formats purl as string | ||
## Examples | ||
iex> Purl.to_string(%Purl{type: "hex", name: "purl"}) | ||
"pkg:hex/purl" | ||
""" | ||
@spec to_string(purl :: t()) :: String.t() | ||
def to_string(%Purl{} = purl), do: purl |> to_uri() |> URI.to_string() | ||
|
||
# @doc """ | ||
# Converts a purl to a `URI` | ||
|
||
# ## Examples | ||
|
||
# iex> Purl.to_uri(%Purl{type: "hex", name: "purl"}) | ||
# %URI{scheme: "pkg", path: "hex/purl"} | ||
|
||
# """ | ||
@spec to_uri(purl :: t()) :: URI.t() | ||
defdelegate to_uri(purl), to: Composer, as: :compose_uri | ||
|
||
@doc """ | ||
Hello world. | ||
Creates a new purl struct from a `Purl`, `URI` or string. | ||
## Examples | ||
iex> Purl.hello() | ||
:world | ||
iex> Purl.new("pkg:hex/purl") | ||
{:ok, %Purl{type: "hex", name: "purl"}} | ||
""" | ||
@spec hello :: :world | ||
def hello do | ||
:world | ||
@spec new(purl :: String.t() | URI.t() | Purl.t()) :: | ||
{:ok, Purl.t()} | {:error, parse_error() | Purl.Error.SpecialCaseFailed.t()} | ||
def new(purl) do | ||
with {:ok, purl} <- Parser.parse(purl), | ||
{:ok, purl} <- SpecialCase.apply(purl) do | ||
{:ok, purl} | ||
end | ||
end | ||
|
||
@doc """ | ||
Similar to `new/1` but raises `URI.Error`, `Purl.Error.InvalidField` or | ||
`Purl.Error.InvalidURI` if an invalid input is given. | ||
## Examples | ||
iex> Purl.new!("pkg:hex/purl") | ||
%Purl{type: "hex", name: "purl"} | ||
iex> Purl.new!(">pkg:hex/purl") | ||
** (URI.Error) cannot parse due to reason invalid_uri: ">" | ||
iex> Purl.new!("pkg:hex*/purl") | ||
** (Purl.Error.InvalidField) invalid field type, \"hex*\" given | ||
""" | ||
@spec new!(purl :: String.t() | URI.t() | Purl.t()) :: Purl.t() | ||
def new!(purl) do | ||
case new(purl) do | ||
{:ok, purl} -> purl | ||
{:error, reason} -> raise reason | ||
end | ||
end | ||
|
||
defimpl String.Chars do | ||
@impl String.Chars | ||
def to_string(%Purl{} = purl), do: Purl.to_string(purl) | ||
end | ||
|
||
defimpl Inspect do | ||
import Inspect.Algebra | ||
|
||
@impl Inspect | ||
def inspect(%Purl{} = purl, opts) do | ||
concat(["Purl.parse!(", to_doc(Purl.to_string(purl), opts), ")"]) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
defmodule Purl.Composer do | ||
@moduledoc false | ||
|
||
@spec compose_uri(purl :: Purl.t()) :: URI.t() | ||
def compose_uri(purl) | ||
|
||
def compose_uri(%Purl{version: %Version{} = version} = purl), | ||
do: compose_uri(%Purl{purl | version: Version.to_string(version)}) | ||
|
||
def compose_uri(%Purl{ | ||
type: type, | ||
namespace: namespace, | ||
name: name, | ||
version: version, | ||
qualifiers: qualifiers, | ||
subpath: subpath | ||
}) do | ||
%URI{ | ||
scheme: "pkg", | ||
path: | ||
Enum.join( | ||
[type | encode_namespace(namespace)] ++ | ||
[ | ||
case version do | ||
nil -> name | ||
version -> "#{name}@#{URI.encode(version)}" | ||
end | ||
], | ||
"/" | ||
), | ||
query: | ||
unless qualifiers == %{} do | ||
encode_qualifiers(qualifiers) | ||
end, | ||
fragment: | ||
unless subpath == [] do | ||
Enum.join(subpath, "/") | ||
end | ||
} | ||
end | ||
|
||
@spec encode_namespace(namespace :: Purl.namespace()) :: [String.t()] | ||
defp encode_namespace(namespace) do | ||
Enum.map(namespace, fn namespace_segment -> | ||
URI.encode(namespace_segment, &(&1 != ?@ and URI.char_unescaped?(&1))) | ||
end) | ||
end | ||
|
||
@spec encode_qualifiers(qualifiers :: Purl.qualifiers()) :: String.t() | ||
defp encode_qualifiers(qualifiers) do | ||
Enum.map_join(qualifiers, "&", fn {key, value} -> | ||
URI.encode(key, &URI.char_unreserved?/1) <> | ||
"=" <> URI.encode(value, &(&1 == ?/ or URI.char_unescaped?(&1))) | ||
end) | ||
end | ||
end |
Oops, something went wrong.