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(<>, buffer, acc) do lexer(rest, <>, 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?(<>), do: true defp leading_zero?(_), do: false defp parse_digits(<>, acc) when char in ?0..?9, do: parse_digits(rest, <>) 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?(<>) 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