defmodule HexSolver.Requirement do
@moduledoc false
alias HexSolver.Constraints.{Range, Util}
alias HexSolver.Requirement.Parser
@allowed_range_ops [:>, :>=, :<, :<=, :~>]
def to_constraint(string) when is_binary(string) do
case Parser.parse(string) do
{:ok, lexed} -> {:ok, delex(lexed, [])}
:error -> :error
end
catch
{__MODULE__, :invalid_constraint} ->
:error
end
def to_constraint(%Elixir.Version{} = version) do
{:ok, version}
end
def to_constraint(%Elixir.Version.Requirement{} = requirement) do
to_constraint(to_string(requirement))
end
def to_constraint!(string) when is_binary(string) do
case Parser.parse(string) do
{:ok, lexed} -> delex(lexed, [])
:error -> raise Elixir.Version.InvalidRequirementError, string
end
catch
{__MODULE__, :invalid_constraint} ->
raise Elixir.Version.InvalidRequirementError, string
end
def to_constraint!(%Elixir.Version{} = version) do
%{version | build: nil}
end
def to_constraint!(%Elixir.Version.Requirement{} = requirement) do
to_constraint!(to_string(requirement))
end
defp delex([], acc) do
Util.union(acc)
end
defp delex([op | rest], acc) when op in [:||, :or] do
delex(rest, acc)
end
defp delex([op1, version1, op, op2, version2 | rest], acc) when op in [:&&, :and] do
range = to_range(op1, version1, op2, version2)
delex(rest, [range | acc])
end
defp delex([op, version | rest], acc) do
range = to_range(op, version)
delex(rest, [range | acc])
end
defp to_range(:==, version) do
to_version(version)
end
defp to_range(:~>, {major, minor, nil, pre, _build}) do
%Range{
min: to_version({major, minor, 0, pre, nil}),
max: to_version({major + 1, 0, 0, [0], nil}),
include_min: true
}
end
defp to_range(:~>, {major, minor, patch, pre, _build}) do
%Range{
min: to_version({major, minor, patch, pre, nil}),
max: to_version({major, minor + 1, 0, [0], nil}),
include_min: true
}
end
defp to_range(:>, version) do
%Range{min: to_version(version)}
end
defp to_range(:>=, version) do
%Range{min: to_version(version), include_min: true}
end
defp to_range(:<, version) do
%Range{max: to_version(version)}
end
defp to_range(:<=, version) do
%Range{max: to_version(version), include_max: true}
end
defp to_range(:~>, _version1, :~>, _version2) do
throw({__MODULE__, :invalid_constraint})
end
defp to_range(op1, version1, :~>, version2) do
to_range(:~>, version2, op1, version1)
end
defp to_range(:~>, version1, op2, version2) do
range1 = to_range(:~>, version1)
range2 = to_range(op2, version2)
range = Range.intersect(range1, range2)
unless Range.valid?(range) and Range.allows_any?(range1, range2) do
throw({__MODULE__, :invalid_constraint})
end
range
end
defp to_range(op1, version1, op2, version2)
when op1 in @allowed_range_ops and op2 in @allowed_range_ops do
range =
Map.merge(to_range(op1, version1), to_range(op2, version2), fn
:__struct__, Range, Range -> Range
:min, nil, value -> value
:min, value, nil -> value
:max, nil, value -> value
:max, value, nil -> value
:include_min, value, value -> value
:include_min, false, value -> value
:include_min, value, false -> value
:include_max, value, value -> value
:include_max, false, value -> value
:include_max, value, false -> value
end)
unless Range.valid?(range) do
throw({__MODULE__, :invalid_constraint})
end
range
end
defp to_version({major, minor, patch, pre, _build}),
do: %Elixir.Version{major: major, minor: minor, patch: patch, pre: pre}
# Vendored from https://github.com/elixir-lang/elixir/blob/0ff6522/lib/elixir/lib/version.ex#L495
defmodule Parser do
@moduledoc false
operators = [
{">=", :>=},
{"<=", :<=},
{"~>", :~>},
{">", :>},
{"<", :<},
{"==", :==},
{" or ", :or},
{" and ", :and}
]
def parse(string) do
revert_lexed(lexer(string), [])
end
defp lexer(string) do
lexer(string, "", [])
end
for {string_op, atom_op} <- operators do
defp lexer(unquote(string_op) <> rest, buffer, acc) do
lexer(rest, "", [unquote(atom_op) | maybe_prepend_buffer(buffer, acc)])
end
end
defp lexer(" " <> rest, buffer, acc) do
lexer(rest, "", maybe_prepend_buffer(buffer, acc))
end
defp lexer(<<char::utf8, rest::binary>>, buffer, acc) do
lexer(rest, <<buffer::binary, char::utf8>>, acc)
end
defp lexer(<<>>, buffer, acc) do
maybe_prepend_buffer(buffer, acc)
end
defp maybe_prepend_buffer("", acc), do: acc
defp maybe_prepend_buffer(buffer, [head | _] = acc)
when is_atom(head) and head not in [:and, :or],
do: [buffer | acc]
defp maybe_prepend_buffer(buffer, acc),
do: [buffer, :== | acc]
defp revert_lexed([version, op, cond | rest], acc)
when is_binary(version) and is_atom(op) and cond in [:or, :and] do
with {:ok, version} <- validate_requirement(op, version) do
revert_lexed(rest, [cond, op, version | acc])
end
end
defp revert_lexed([version, op], acc) when is_binary(version) and is_atom(op) do
with {:ok, version} <- validate_requirement(op, version) do
{:ok, [op, version | acc]}
end
end
defp revert_lexed(_rest, _acc), do: :error
defp validate_requirement(op, version) do
case parse_version(version, true) do
{:ok, version} when op == :~> -> {:ok, version}
{:ok, {_, _, patch, _, _} = version} when is_integer(patch) -> {:ok, version}
_ -> :error
end
end
defp parse_version(string, approximate?) when is_binary(string) do
destructure [version_with_pre, build], String.split(string, "+", parts: 2)
destructure [version, pre], String.split(version_with_pre, "-", parts: 2)
destructure [major, minor, patch, next], String.split(version, ".")
with nil <- next,
{:ok, major} <- require_digits(major),
{:ok, minor} <- require_digits(minor),
{:ok, patch} <- maybe_patch(patch, approximate?),
{:ok, pre_parts} <- optional_dot_separated(pre),
{:ok, pre_parts} <- convert_parts_to_integer(pre_parts, []),
{:ok, build_parts} <- optional_dot_separated(build) do
{:ok, {major, minor, patch, pre_parts, build_parts}}
else
_other -> :error
end
end
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string), do: :error, else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
defp leading_zero?(_), do: false
defp parse_digits(<<char, rest::binary>>, acc) when char in ?0..?9,
do: parse_digits(rest, <<acc::binary, char>>)
defp parse_digits(<<>>, acc) when byte_size(acc) > 0, do: {:ok, String.to_integer(acc)}
defp parse_digits(_, _acc), do: :error
defp maybe_patch(patch, approximate?)
defp maybe_patch(nil, true), do: {:ok, nil}
defp maybe_patch(patch, _), do: require_digits(patch)
defp optional_dot_separated(nil), do: {:ok, []}
defp optional_dot_separated(string) do
parts = String.split(string, ".")
if Enum.all?(parts, &(&1 != "" and valid_identifier?(&1))) do
{:ok, parts}
else
:error
end
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
if leading_zero?(part) do
:error
else
convert_parts_to_integer(rest, [integer | acc])
end
:error ->
convert_parts_to_integer(rest, [part | acc])
end
end
defp convert_parts_to_integer([], acc) do
{:ok, Enum.reverse(acc)}
end
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z
when char in ?A..?Z
when char == ?- do
valid_identifier?(rest)
end
defp valid_identifier?(<<>>) do
true
end
defp valid_identifier?(_other) do
false
end
end
end