1defmodule Hex.Version do
2  @moduledoc false
3
4  defmodule Requirement do
5    @moduledoc false
6    defstruct [:source, :req]
7  end
8
9  defmodule InvalidRequirementError do
10    @moduledoc false
11    defexception [:requirement]
12
13    def exception(requirement) when is_binary(requirement) do
14      %__MODULE__{requirement: requirement}
15    end
16
17    def message(%{requirement: requirement}) do
18      "invalid requirement: #{inspect(requirement)}"
19    end
20  end
21
22  defmodule InvalidVersionError do
23    @moduledoc false
24    defexception [:version]
25
26    def exception(version) when is_binary(version) do
27      %__MODULE__{version: version}
28    end
29
30    def message(%{version: version}) do
31      "invalid version: #{inspect(version)}"
32    end
33  end
34
35  @ets :hex_version
36
37  def start do
38    :ets.new(@ets, [:named_table, :public])
39    {:ok, []}
40  end
41
42  def stable?(%Version{pre: []}), do: true
43  def stable?(%Version{}), do: false
44  def stable?(other), do: stable?(parse!(other))
45
46  def match?(version, requirement, opts \\ []) do
47    allow_pre = Keyword.get(opts, :allow_pre, false)
48    req_source = requirement_source(requirement)
49
50    cache({:match?, version, req_source, allow_pre}, fn ->
51      version = parse!(version)
52      requirement = parse_requirement!(req_source, allow_pre: allow_pre)
53
54      cond do
55        allow_pre_available?() ->
56          Version.match?(version, requirement, allow_pre: allow_pre)
57
58        allow_pre ->
59          Version.match?(version, requirement)
60
61        true ->
62          custom_match?(version, requirement)
63      end
64    end)
65  end
66
67  def compare(version1, version2) do
68    cache({:compare, version1, version2}, fn ->
69      version1 = parse!(version1)
70      version2 = parse!(version2)
71      Version.compare(version1, version2)
72    end)
73  end
74
75  def parse(%Version{} = version) do
76    {:ok, version}
77  end
78
79  def parse(version) do
80    cache({:version, version}, fn ->
81      Version.parse(version)
82    end)
83  end
84
85  def parse!(version) do
86    case parse(version) do
87      {:ok, version} ->
88        version
89
90      :error ->
91        raise InvalidVersionError, version
92    end
93  end
94
95  def parse_requirement(req, opts \\ [])
96
97  def parse_requirement(%Requirement{} = req, _opts) do
98    {:ok, req}
99  end
100
101  def parse_requirement(%Version.Requirement{} = req, _opts) do
102    {:ok, req}
103  end
104
105  def parse_requirement(requirement, opts) do
106    allow_pre = Keyword.get(opts, :allow_pre, false)
107
108    cache({:req, requirement, allow_pre}, fn ->
109      if allow_pre or allow_pre_available?() do
110        case Version.parse_requirement(requirement) do
111          {:ok, req} -> {:ok, compile_requirement(req)}
112          :error -> :error
113        end
114      else
115        custom_requirement(requirement)
116      end
117    end)
118  end
119
120  defp compile_requirement(req) do
121    if allow_pre_available?() do
122      Version.compile_requirement(req)
123    else
124      req
125    end
126  end
127
128  def parse_requirement!(requirement, opts \\ []) do
129    case parse_requirement(requirement, opts) do
130      {:ok, requirement} ->
131        requirement
132
133      :error ->
134        raise InvalidRequirementError, requirement
135    end
136  end
137
138  defp cache(key, fun) do
139    case :ets.lookup(@ets, key) do
140      [{_, value}] ->
141        value
142
143      [] ->
144        value = fun.()
145        :ets.insert(@ets, {key, value})
146        value
147    end
148  end
149
150  defp requirement_source(%Requirement{source: source}), do: source
151  defp requirement_source(%Version.Requirement{source: source}), do: source
152  defp requirement_source(source), do: source
153
154  defp custom_match?(version, %Requirement{req: req}) do
155    custom_match?(version, req)
156  end
157
158  defp custom_match?(version, {"and", x, y}) do
159    custom_match?(version, x) and custom_match?(version, y)
160  end
161
162  defp custom_match?(version, {"or", x, y}) do
163    custom_match?(version, x) or custom_match?(version, y)
164  end
165
166  defp custom_match?(version, {%Version.Requirement{} = req, true}) do
167    Version.match?(version, req)
168  end
169
170  defp custom_match?(%Version{pre: []} = version, {%Version.Requirement{} = req, false}) do
171    Version.match?(version, req)
172  end
173
174  defp custom_match?(_version, _req) do
175    false
176  end
177
178  defp custom_requirement(requirement) do
179    try do
180      req =
181        requirement
182        |> String.split(" ", trim: true)
183        |> split_ops()
184        |> custom_parse()
185
186      {:ok, %Requirement{source: requirement, req: req}}
187    catch
188      :error ->
189        :error
190    end
191  end
192
193  @version_ops ~w(~> == != <= >= < >)
194  @bool_ops ~w(and or)
195
196  defp custom_parse([op, version]) when op in @version_ops do
197    pre? = String.contains?(version, "-")
198
199    case Version.parse_requirement(op <> " " <> version) do
200      {:ok, req} ->
201        {req, pre?}
202
203      :error ->
204        throw(:error)
205    end
206  end
207
208  defp custom_parse([op1, version, op2 | rest]) when op2 in @bool_ops do
209    {op2, custom_parse([op1, version]), custom_parse(rest)}
210  end
211
212  defp custom_parse([version]) do
213    custom_parse(["==", version])
214  end
215
216  defp custom_parse(_) do
217    throw(:error)
218  end
219
220  def split_ops([op | rest]) when op in @version_ops do
221    [op | split_ops(rest)]
222  end
223
224  def split_ops([<<op::binary-2, version::binary>> | rest]) when op in @version_ops do
225    [op, version | split_ops(rest)]
226  end
227
228  def split_ops([<<op::binary-1, version::binary>> | rest]) when op in @version_ops do
229    [op, version | split_ops(rest)]
230  end
231
232  def split_ops([version | rest]) do
233    [version | split_ops(rest)]
234  end
235
236  def split_ops([]) do
237    []
238  end
239
240  defp allow_pre_available? do
241    Code.ensure_loaded?(Version) and function_exported?(Version, :match?, 3)
242  end
243
244  def major_version_change?(%Version{} = version1, %Version{} = version2) do
245    version1.major != version2.major
246  end
247
248  def breaking_minor_version_change?(%Version{} = version1, %Version{} = version2) do
249    version1.major == 0 and version2.major == 0 and version1.minor != version2.minor
250  end
251end
252