1
2local vers = {}
3
4local util = require("luarocks.core.util")
5local require = nil
6--------------------------------------------------------------------------------
7
8local deltas = {
9   dev =    120000000,
10   scm =    110000000,
11   cvs =    100000000,
12   rc =    -1000,
13   pre =   -10000,
14   beta =  -100000,
15   alpha = -1000000
16}
17
18local version_mt = {
19   --- Equality comparison for versions.
20   -- All version numbers must be equal.
21   -- If both versions have revision numbers, they must be equal;
22   -- otherwise the revision number is ignored.
23   -- @param v1 table: version table to compare.
24   -- @param v2 table: version table to compare.
25   -- @return boolean: true if they are considered equivalent.
26   __eq = function(v1, v2)
27      if #v1 ~= #v2 then
28         return false
29      end
30      for i = 1, #v1 do
31         if v1[i] ~= v2[i] then
32            return false
33         end
34      end
35      if v1.revision and v2.revision then
36         return (v1.revision == v2.revision)
37      end
38      return true
39   end,
40   --- Size comparison for versions.
41   -- All version numbers are compared.
42   -- If both versions have revision numbers, they are compared;
43   -- otherwise the revision number is ignored.
44   -- @param v1 table: version table to compare.
45   -- @param v2 table: version table to compare.
46   -- @return boolean: true if v1 is considered lower than v2.
47   __lt = function(v1, v2)
48      for i = 1, math.max(#v1, #v2) do
49         local v1i, v2i = v1[i] or 0, v2[i] or 0
50         if v1i ~= v2i then
51            return (v1i < v2i)
52         end
53      end
54      if v1.revision and v2.revision then
55         return (v1.revision < v2.revision)
56      end
57      return false
58   end,
59   -- @param v1 table: version table to compare.
60   -- @param v2 table: version table to compare.
61   -- @return boolean: true if v1 is considered lower than or equal to v2.
62   __le = function(v1, v2)
63       return not (v2 < v1)
64   end,
65   --- Return version as a string.
66   -- @param v The version table.
67   -- @return The string representation.
68   __tostring = function(v)
69      return v.string
70   end,
71}
72
73local version_cache = {}
74setmetatable(version_cache, {
75   __mode = "kv"
76})
77
78--- Parse a version string, converting to table format.
79-- A version table contains all components of the version string
80-- converted to numeric format, stored in the array part of the table.
81-- If the version contains a revision, it is stored numerically
82-- in the 'revision' field. The original string representation of
83-- the string is preserved in the 'string' field.
84-- Returned version tables use a metatable
85-- allowing later comparison through relational operators.
86-- @param vstring string: A version number in string format.
87-- @return table or nil: A version table or nil
88-- if the input string contains invalid characters.
89function vers.parse_version(vstring)
90   if not vstring then return nil end
91   assert(type(vstring) == "string")
92
93   local cached = version_cache[vstring]
94   if cached then
95      return cached
96   end
97
98   local version = {}
99   local i = 1
100
101   local function add_token(number)
102      version[i] = version[i] and version[i] + number/100000 or number
103      i = i + 1
104   end
105
106   -- trim leading and trailing spaces
107   local v = vstring:match("^%s*(.*)%s*$")
108   version.string = v
109   -- store revision separately if any
110   local main, revision = v:match("(.*)%-(%d+)$")
111   if revision then
112      v = main
113      version.revision = tonumber(revision)
114   end
115   while #v > 0 do
116      -- extract a number
117      local token, rest = v:match("^(%d+)[%.%-%_]*(.*)")
118      if token then
119         add_token(tonumber(token))
120      else
121         -- extract a word
122         token, rest = v:match("^(%a+)[%.%-%_]*(.*)")
123         if not token then
124            util.warning("version number '"..v.."' could not be parsed.")
125            version[i] = 0
126            break
127         end
128         version[i] = deltas[token] or (token:byte() / 1000)
129      end
130      v = rest
131   end
132   setmetatable(version, version_mt)
133   version_cache[vstring] = version
134   return version
135end
136
137--- Utility function to compare version numbers given as strings.
138-- @param a string: one version.
139-- @param b string: another version.
140-- @return boolean: True if a > b.
141function vers.compare_versions(a, b)
142   if a == b then
143      return false
144   end
145   return vers.parse_version(a) > vers.parse_version(b)
146end
147
148--- A more lenient check for equivalence between versions.
149-- This returns true if the requested components of a version
150-- match and ignore the ones that were not given. For example,
151-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
152-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
153-- doesn't.
154-- @param version string or table: Version to be tested; may be
155-- in string format or already parsed into a table.
156-- @param requested string or table: Version requested; may be
157-- in string format or already parsed into a table.
158-- @return boolean: True if the tested version matches the requested
159-- version, false otherwise.
160local function partial_match(version, requested)
161   assert(type(version) == "string" or type(version) == "table")
162   assert(type(requested) == "string" or type(version) == "table")
163
164   if type(version) ~= "table" then version = vers.parse_version(version) end
165   if type(requested) ~= "table" then requested = vers.parse_version(requested) end
166   if not version or not requested then return false end
167
168   for i, ri in ipairs(requested) do
169      local vi = version[i] or 0
170      if ri ~= vi then return false end
171   end
172   if requested.revision then
173      return requested.revision == version.revision
174   end
175   return true
176end
177
178--- Check if a version satisfies a set of constraints.
179-- @param version table: A version in table format
180-- @param constraints table: An array of constraints in table format.
181-- @return boolean: True if version satisfies all constraints,
182-- false otherwise.
183function vers.match_constraints(version, constraints)
184   assert(type(version) == "table")
185   assert(type(constraints) == "table")
186   local ok = true
187   setmetatable(version, version_mt)
188   for _, constr in pairs(constraints) do
189      if type(constr.version) == "string" then
190         constr.version = vers.parse_version(constr.version)
191      end
192      local constr_version, constr_op = constr.version, constr.op
193      setmetatable(constr_version, version_mt)
194      if     constr_op == "==" then ok = version == constr_version
195      elseif constr_op == "~=" then ok = version ~= constr_version
196      elseif constr_op == ">"  then ok = version >  constr_version
197      elseif constr_op == "<"  then ok = version <  constr_version
198      elseif constr_op == ">=" then ok = version >= constr_version
199      elseif constr_op == "<=" then ok = version <= constr_version
200      elseif constr_op == "~>" then ok = partial_match(version, constr_version)
201      end
202      if not ok then break end
203   end
204   return ok
205end
206
207return vers
208