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