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 	Increments a given version number to the next incompatible version.
256 
257 	Prerelease and build metadata information is removed.
258 
259 	This implements the "^" comparison operator, which represents "nonbreaking semver compatibility."
260 	With 0.x.y releases, any release can break.
261 	With x.y.z releases, only major releases can break.
262 */
263 string bumpIncompatibleVersion(string ver)
264 pure {
265 	// Cut off metadata and prerelease information.
266 	auto mi = ver.indexOfAny("+-");
267 	if (mi > 0) ver = ver[0..mi];
268 	// Increment next to last version from a[.b[.c]].
269 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
270 	assert(splitted.length == 3, "Version corrupt: " ~ ver);
271 	if (splitted[0] == "0") splitted[2] = to!string(to!int(splitted[2]) + 1);
272 	else splitted = [to!string(to!int(splitted[0]) + 1), "0", "0"];
273 	return splitted.join(".");
274 }
275 ///
276 unittest {
277 	assert(bumpIncompatibleVersion("0.0.0") == "0.0.1");
278 	assert(bumpIncompatibleVersion("0.1.2") == "0.1.3");
279 	assert(bumpIncompatibleVersion("1.0.0") == "2.0.0");
280 	assert(bumpIncompatibleVersion("1.2.3") == "2.0.0");
281 	assert(bumpIncompatibleVersion("1.2.3+metadata") == "2.0.0");
282 	assert(bumpIncompatibleVersion("1.2.3-pre.release") == "2.0.0");
283 	assert(bumpIncompatibleVersion("1.2.3-pre.release+metadata") == "2.0.0");
284 }
285 
286 /**
287 	Takes a partial version and expands it to a valid SemVer version.
288 
289 	This function corresponds to the semantivs of the "~>" comparison operator's
290 	lower bound.
291 
292 	See_Also: `bumpVersion`
293 */
294 string expandVersion(string ver)
295 pure {
296 	auto mi = ver.indexOfAny("+-");
297 	auto sub = "";
298 	if (mi > 0) {
299 		sub = ver[mi..$];
300 		ver = ver[0..mi];
301 	}
302 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
303 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
304 	while (splitted.length < 3) splitted ~= "0";
305 	return splitted.join(".") ~ sub;
306 }
307 ///
308 unittest {
309 	assert("1.0.0" == expandVersion("1"));
310 	assert("1.0.0" == expandVersion("1.0"));
311 	assert("1.0.0" == expandVersion("1.0.0"));
312 	// These are rather excotic variants...
313 	assert("1.0.0-pre.release" == expandVersion("1-pre.release"));
314 	assert("1.0.0+meta" == expandVersion("1+meta"));
315 	assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta"));
316 }
317 
318 private int compareIdentifier(ref string a, ref string b)
319 pure @nogc {
320 	bool anumber = true;
321 	bool bnumber = true;
322 	bool aempty = true, bempty = true;
323 	int res = 0;
324 	while (true) {
325 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
326 		if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false;
327 		if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false;
328 		a = a[1 .. $]; b = b[1 .. $];
329 		aempty = !a.length || a[0] == '.' || a[0] == '+';
330 		bempty = !b.length || b[0] == '.' || b[0] == '+';
331 		if (aempty || bempty) break;
332 	}
333 
334 	if (anumber && bnumber) {
335 		// the !empty value might be an indentifier instead of a number, but identifiers always have precedence
336 		if (aempty != bempty) return bempty - aempty;
337 		return res;
338 	} else {
339 		if (anumber && aempty) return -1;
340 		if (bnumber && bempty) return 1;
341 		// this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
342 		static assert('0' < 'a' && '0' < 'A');
343 		if (res != 0) return res;
344 		return bempty - aempty;
345 	}
346 }
347 
348 private int compareNumber(ref string a, ref string b)
349 pure @nogc {
350 	int res = 0;
351 	while (true) {
352 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
353 		a = a[1 .. $]; b = b[1 .. $];
354 		auto aempty = !a.length || (a[0] < '0' || a[0] > '9');
355 		auto bempty = !b.length || (b[0] < '0' || b[0] > '9');
356 		if (aempty != bempty) return bempty - aempty;
357 		if (aempty) return res;
358 	}
359 }
360 
361 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
362 pure @nogc {
363 	if (str.length == 0) return false;
364 	while (str.length) {
365 		auto end = str.indexOf('.');
366 		if (end < 0) end = str.length;
367 		if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
368 		if (end < str.length) str = str[end+1 .. $];
369 		else break;
370 	}
371 	return true;
372 }
373 
374 private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
375 pure @nogc {
376 	if (str.length < 1) return false;
377 
378 	bool numeric = true;
379 	foreach (ch; str) {
380 		switch (ch) {
381 			default: return false;
382 			case 'a': .. case 'z':
383 			case 'A': .. case 'Z':
384 			case '-':
385 				numeric = false;
386 				break;
387 			case '0': .. case '9':
388 				break;
389 		}
390 	}
391 
392 	if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
393 
394 	return true;
395 }
396 
397 private bool isValidNumber(string str)
398 pure @nogc {
399 	if (str.length < 1) return false;
400 	foreach (ch; str)
401 		if (ch < '0' || ch > '9')
402 			return false;
403 
404 	// don't allow leading zeros
405 	if (str[0] == '0' && str.length > 1) return false;
406 
407 	return true;
408 }
409 
410 private ptrdiff_t indexOfAny(string str, in char[] chars)
411 pure @nogc {
412 	ptrdiff_t ret = -1;
413 	foreach (ch; chars) {
414 		auto idx = str.indexOf(ch);
415 		if (idx >= 0 && (ret < 0 || idx < ret))
416 			ret = idx;
417 	}
418 	return ret;
419 }