1 /**
2 	Implementes version validation and comparison according to the semantic
3 	versioning specification.
4 
5 	The general format of a semantiv version is: a.b.c[-x.y...][+x.y...]
6 	a/b/c must be integer numbers with no leading zeros, and x/y/... must be
7 	either numbers or identifiers containing only ASCII alphabetic characters
8 	or hyphens. Identifiers may not start with a digit.
9 
10 	See_Also: http://semver.org/
11 
12 	Copyright: © 2013-2016 rejectedsoftware e.K.
13 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
14 	Authors: Sönke Ludwig
15 */
16 module dub.semver;
17 
18 import std.range;
19 import std.string;
20 import std.algorithm : max;
21 import std.conv;
22 
23 @safe:
24 
25 /**
26 	Validates a version string according to the SemVer specification.
27 */
28 bool isValidVersion(string ver)
29 pure @nogc {
30 	// NOTE: this is not by spec, but to ensure sane input
31 	if (ver.length > 256) return false;
32 
33 	// a
34 	auto sepi = ver.indexOf('.');
35 	if (sepi < 0) return false;
36 	if (!isValidNumber(ver[0 .. sepi])) return false;
37 	ver = ver[sepi+1 .. $];
38 
39 	// c
40 	sepi = ver.indexOf('.');
41 	if (sepi < 0) return false;
42 	if (!isValidNumber(ver[0 .. sepi])) return false;
43 	ver = ver[sepi+1 .. $];
44 
45 	// c
46 	sepi = ver.indexOfAny("-+");
47 	if (sepi < 0) sepi = ver.length;
48 	if (!isValidNumber(ver[0 .. sepi])) return false;
49 	ver = ver[sepi .. $];
50 
51 	// prerelease tail
52 	if (ver.length > 0 && ver[0] == '-') {
53 		ver = ver[1 .. $];
54 		sepi = ver.indexOf('+');
55 		if (sepi < 0) sepi = ver.length;
56 		if (!isValidIdentifierChain(ver[0 .. sepi])) return false;
57 		ver = ver[sepi .. $];
58 	}
59 
60 	// build tail
61 	if (ver.length > 0 && ver[0] == '+') {
62 		ver = ver[1 .. $];
63 		if (!isValidIdentifierChain(ver, true)) return false;
64 		ver = null;
65 	}
66 
67 	assert(ver.length == 0);
68 	return true;
69 }
70 
71 ///
72 unittest {
73 	assert(isValidVersion("1.9.0"));
74 	assert(isValidVersion("0.10.0"));
75 	assert(!isValidVersion("01.9.0"));
76 	assert(!isValidVersion("1.09.0"));
77 	assert(!isValidVersion("1.9.00"));
78 	assert(isValidVersion("1.0.0-alpha"));
79 	assert(isValidVersion("1.0.0-alpha.1"));
80 	assert(isValidVersion("1.0.0-0.3.7"));
81 	assert(isValidVersion("1.0.0-x.7.z.92"));
82 	assert(isValidVersion("1.0.0-x.7-z.92"));
83 	assert(!isValidVersion("1.0.0-00.3.7"));
84 	assert(!isValidVersion("1.0.0-0.03.7"));
85 	assert(isValidVersion("1.0.0-alpha+001"));
86 	assert(isValidVersion("1.0.0+20130313144700"));
87 	assert(isValidVersion("1.0.0-beta+exp.sha.5114f85"));
88 	assert(!isValidVersion(" 1.0.0"));
89 	assert(!isValidVersion("1. 0.0"));
90 	assert(!isValidVersion("1.0 .0"));
91 	assert(!isValidVersion("1.0.0 "));
92 	assert(!isValidVersion("1.0.0-a_b"));
93 	assert(!isValidVersion("1.0.0+"));
94 	assert(!isValidVersion("1.0.0-"));
95 	assert(!isValidVersion("1.0.0-+a"));
96 	assert(!isValidVersion("1.0.0-a+"));
97 	assert(!isValidVersion("1.0"));
98 	assert(!isValidVersion("1.0-1.0"));
99 }
100 
101 
102 /**
103 	Determines if a given valid SemVer version has a pre-release suffix.
104 */
105 bool isPreReleaseVersion(string ver) pure @nogc
106 in { assert(isValidVersion(ver)); }
107 body {
108 	foreach (i; 0 .. 2) {
109 		auto di = ver.indexOf('.');
110 		assert(di > 0);
111 		ver = ver[di+1 .. $];
112 	}
113 	auto di = ver.indexOf('-');
114 	if (di < 0) return false;
115 	return isValidNumber(ver[0 .. di]);
116 }
117 
118 ///
119 unittest {
120 	assert(isPreReleaseVersion("1.0.0-alpha"));
121 	assert(isPreReleaseVersion("1.0.0-alpha+b1"));
122 	assert(isPreReleaseVersion("0.9.0-beta.1"));
123 	assert(!isPreReleaseVersion("0.9.0"));
124 	assert(!isPreReleaseVersion("0.9.0+b1"));
125 }
126 
127 /**
128 	Compares the precedence of two SemVer version strings.
129 
130 	The version strings must be validated using `isValidVersion` before being
131 	passed to this function. Note that the build meta data suffix (if any) is
132 	being ignored when comparing version numbers.
133 
134 	Returns:
135 		Returns a negative number if `a` is a lower version than `b`, `0` if they are
136 		equal, and a positive number otherwise.
137 */
138 int compareVersions(string a, string b)
139 pure @nogc {
140 	// compare a.b.c numerically
141 	if (auto ret = compareNumber(a, b)) return ret;
142 	assert(a[0] == '.' && b[0] == '.');
143 	a = a[1 .. $]; b = b[1 .. $];
144 	if (auto ret = compareNumber(a, b)) return ret;
145 	assert(a[0] == '.' && b[0] == '.');
146 	a = a[1 .. $]; b = b[1 .. $];
147 	if (auto ret = compareNumber(a, b)) return ret;
148 
149 	// give precedence to non-prerelease versions
150 	bool apre = a.length > 0 && a[0] == '-';
151 	bool bpre = b.length > 0 && b[0] == '-';
152 	if (apre != bpre) return bpre - apre;
153 	if (!apre) return 0;
154 
155 	// compare the prerelease tail lexicographically
156 	do {
157 		a = a[1 .. $]; b = b[1 .. $];
158 		if (auto ret = compareIdentifier(a, b)) return ret;
159 	} while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+');
160 
161 	// give longer prerelease tails precedence
162 	bool aempty = a.length == 0 || a[0] == '+';
163 	bool bempty = b.length == 0 || b[0] == '+';
164 	if (aempty == bempty) {
165 		assert(aempty);
166 		return 0;
167 	}
168 	return bempty - aempty;
169 }
170 
171 ///
172 unittest {
173 	assert(compareVersions("1.0.0", "1.0.0") == 0);
174 	assert(compareVersions("1.0.0+b1", "1.0.0+b2") == 0);
175 	assert(compareVersions("1.0.0", "2.0.0") < 0);
176 	assert(compareVersions("1.0.0-beta", "1.0.0") < 0);
177 	assert(compareVersions("1.0.1", "1.0.0") > 0);
178 }
179 
180 unittest {
181 	void assertLess(string a, string b) {
182 		assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b);
183 		assert(compareVersions(b, a) > 0);
184 		assert(compareVersions(a, a) == 0);
185 		assert(compareVersions(b, b) == 0);
186 	}
187 	assertLess("1.0.0", "2.0.0");
188 	assertLess("2.0.0", "2.1.0");
189 	assertLess("2.1.0", "2.1.1");
190 	assertLess("1.0.0-alpha", "1.0.0");
191 	assertLess("1.0.0-alpha", "1.0.0-alpha.1");
192 	assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta");
193 	assertLess("1.0.0-alpha.beta", "1.0.0-beta");
194 	assertLess("1.0.0-beta", "1.0.0-beta.2");
195 	assertLess("1.0.0-beta.2", "1.0.0-beta.11");
196 	assertLess("1.0.0-beta.11", "1.0.0-rc.1");
197 	assertLess("1.0.0-rc.1", "1.0.0");
198 	assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0);
199 	assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0);
200 	assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0);
201 	assertLess("2.0.0", "10.0.0");
202 	assertLess("1.0.0-2", "1.0.0-10");
203 	assertLess("1.0.0-99", "1.0.0-1a");
204 	assertLess("1.0.0-99", "1.0.0-a");
205 	assertLess("1.0.0-alpha", "1.0.0-alphb");
206 	assertLess("1.0.0-alphz", "1.0.0-alphz0");
207 	assertLess("1.0.0-alphZ", "1.0.0-alpha");
208 }
209 
210 
211 /**
212 	Increments a given (partial) version number to the next higher version.
213 
214 	Prerelease and build metadata information is ignored. The given version
215 	can skip the minor and patch digits. If no digits are skipped, the next
216 	minor version will be selected. If the patch or minor versions are skipped,
217 	the next major version will be selected.
218 
219 	This function corresponds to the semantivs of the "~>" comparison operator's
220 	upper bound.
221 
222 	The semantics of this are the same as for the "approximate" version
223 	specifier from rubygems.
224 	(https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb)
225 
226 	See_Also: `expandVersion`
227 */
228 string bumpVersion(string ver)
229 pure {
230 	// Cut off metadata and prerelease information.
231 	auto mi = ver.indexOfAny("+-");
232 	if (mi > 0) ver = ver[0..mi];
233 	// Increment next to last version from a[.b[.c]].
234 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
235 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
236 	auto to_inc = splitted.length == 3? 1 : 0;
237 	splitted = splitted[0 .. to_inc+1];
238 	splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
239 	// Fill up to three compontents to make valid SemVer version.
240 	while (splitted.length < 3) splitted ~= "0";
241 	return splitted.join(".");
242 }
243 ///
244 unittest {
245 	assert("1.0.0" == bumpVersion("0"));
246 	assert("1.0.0" == bumpVersion("0.0"));
247 	assert("0.1.0" == bumpVersion("0.0.0"));
248 	assert("1.3.0" == bumpVersion("1.2.3"));
249 	assert("1.3.0" == bumpVersion("1.2.3+metadata"));
250 	assert("1.3.0" == bumpVersion("1.2.3-pre.release"));
251 	assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata"));
252 }
253 
254 /**
255 	Takes a partial version and expands it to a valid SemVer version.
256 
257 	This function corresponds to the semantivs of the "~>" comparison operator's
258 	lower bound.
259 
260 	See_Also: `bumpVersion`
261 */
262 string expandVersion(string ver)
263 pure {
264 	auto mi = ver.indexOfAny("+-");
265 	auto sub = "";
266 	if (mi > 0) {
267 		sub = ver[mi..$];
268 		ver = ver[0..mi];
269 	}
270 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
271 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
272 	while (splitted.length < 3) splitted ~= "0";
273 	return splitted.join(".") ~ sub;
274 }
275 ///
276 unittest {
277 	assert("1.0.0" == expandVersion("1"));
278 	assert("1.0.0" == expandVersion("1.0"));
279 	assert("1.0.0" == expandVersion("1.0.0"));
280 	// These are rather excotic variants...
281 	assert("1.0.0-pre.release" == expandVersion("1-pre.release"));
282 	assert("1.0.0+meta" == expandVersion("1+meta"));
283 	assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta"));
284 }
285 
286 private int compareIdentifier(ref string a, ref string b)
287 pure @nogc {
288 	bool anumber = true;
289 	bool bnumber = true;
290 	bool aempty = true, bempty = true;
291 	int res = 0;
292 	while (true) {
293 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
294 		if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false;
295 		if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false;
296 		a = a[1 .. $]; b = b[1 .. $];
297 		aempty = !a.length || a[0] == '.' || a[0] == '+';
298 		bempty = !b.length || b[0] == '.' || b[0] == '+';
299 		if (aempty || bempty) break;
300 	}
301 
302 	if (anumber && bnumber) {
303 		// the !empty value might be an indentifier instead of a number, but identifiers always have precedence
304 		if (aempty != bempty) return bempty - aempty;
305 		return res;
306 	} else {
307 		if (anumber && aempty) return -1;
308 		if (bnumber && bempty) return 1;
309 		// this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
310 		static assert('0' < 'a' && '0' < 'A');
311 		if (res != 0) return res;
312 		return bempty - aempty;
313 	}
314 }
315 
316 private int compareNumber(ref string a, ref string b)
317 pure @nogc {
318 	int res = 0;
319 	while (true) {
320 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
321 		a = a[1 .. $]; b = b[1 .. $];
322 		auto aempty = !a.length || (a[0] < '0' || a[0] > '9');
323 		auto bempty = !b.length || (b[0] < '0' || b[0] > '9');
324 		if (aempty != bempty) return bempty - aempty;
325 		if (aempty) return res;
326 	}
327 }
328 
329 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
330 pure @nogc {
331 	if (str.length == 0) return false;
332 	while (str.length) {
333 		auto end = str.indexOf('.');
334 		if (end < 0) end = str.length;
335 		if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
336 		if (end < str.length) str = str[end+1 .. $];
337 		else break;
338 	}
339 	return true;
340 }
341 
342 private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
343 pure @nogc {
344 	if (str.length < 1) return false;
345 
346 	bool numeric = true;
347 	foreach (ch; str) {
348 		switch (ch) {
349 			default: return false;
350 			case 'a': .. case 'z':
351 			case 'A': .. case 'Z':
352 			case '-':
353 				numeric = false;
354 				break;
355 			case '0': .. case '9':
356 				break;
357 		}
358 	}
359 
360 	if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
361 
362 	return true;
363 }
364 
365 private bool isValidNumber(string str)
366 pure @nogc {
367 	if (str.length < 1) return false;
368 	foreach (ch; str)
369 		if (ch < '0' || ch > '9')
370 			return false;
371 
372 	// don't allow leading zeros
373 	if (str[0] == '0' && str.length > 1) return false;
374 
375 	return true;
376 }
377 
378 private sizediff_t indexOfAny(string str, in char[] chars)
379 pure @nogc {
380 	sizediff_t ret = -1;
381 	foreach (ch; chars) {
382 		auto idx = str.indexOf(ch);
383 		if (idx >= 0 && (ret < 0 || idx < ret))
384 			ret = idx;
385 	}
386 	return ret;
387 }