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(string ver)
28 pure @nogc {
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(string ver) pure @nogc
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(string a, string b)
138 pure @nogc {
139 	// compare a.b.c numerically
140 	if (auto ret = compareNumber(a, b)) return ret;
141 	assert(a[0] == '.' && b[0] == '.');
142 	a = a[1 .. $]; b = b[1 .. $];
143 	if (auto ret = compareNumber(a, b)) return ret;
144 	assert(a[0] == '.' && b[0] == '.');
145 	a = a[1 .. $]; b = b[1 .. $];
146 	if (auto ret = compareNumber(a, b)) return ret;
147 
148 	// give precedence to non-prerelease versions
149 	bool apre = a.length > 0 && a[0] == '-';
150 	bool bpre = b.length > 0 && b[0] == '-';
151 	if (apre != bpre) return bpre - apre;
152 	if (!apre) return 0;
153 
154 	// compare the prerelease tail lexicographically
155 	do {
156 		a = a[1 .. $]; b = b[1 .. $];
157 		if (auto ret = compareIdentifier(a, b)) return ret;
158 	} while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+');
159 
160 	// give longer prerelease tails precedence
161 	bool aempty = a.length == 0 || a[0] == '+';
162 	bool bempty = b.length == 0 || b[0] == '+';
163 	if (aempty == bempty) {
164 		assert(aempty);
165 		return 0;
166 	}
167 	return bempty - aempty;
168 }
169 
170 ///
171 unittest {
172 	assert(compareVersions("1.0.0", "1.0.0") == 0);
173 	assert(compareVersions("1.0.0+b1", "1.0.0+b2") == 0);
174 	assert(compareVersions("1.0.0", "2.0.0") < 0);
175 	assert(compareVersions("1.0.0-beta", "1.0.0") < 0);
176 	assert(compareVersions("1.0.1", "1.0.0") > 0);
177 }
178 
179 unittest {
180 	void assertLess(string a, string b) {
181 		assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b);
182 		assert(compareVersions(b, a) > 0);
183 		assert(compareVersions(a, a) == 0);
184 		assert(compareVersions(b, b) == 0);
185 	}
186 	assertLess("1.0.0", "2.0.0");
187 	assertLess("2.0.0", "2.1.0");
188 	assertLess("2.1.0", "2.1.1");
189 	assertLess("1.0.0-alpha", "1.0.0");
190 	assertLess("1.0.0-alpha", "1.0.0-alpha.1");
191 	assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta");
192 	assertLess("1.0.0-alpha.beta", "1.0.0-beta");
193 	assertLess("1.0.0-beta", "1.0.0-beta.2");
194 	assertLess("1.0.0-beta.2", "1.0.0-beta.11");
195 	assertLess("1.0.0-beta.11", "1.0.0-rc.1");
196 	assertLess("1.0.0-rc.1", "1.0.0");
197 	assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0);
198 	assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0);
199 	assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0);
200 	assertLess("2.0.0", "10.0.0");
201 	assertLess("1.0.0-2", "1.0.0-10");
202 	assertLess("1.0.0-99", "1.0.0-1a");
203 	assertLess("1.0.0-99", "1.0.0-a");
204 	assertLess("1.0.0-alpha", "1.0.0-alphb");
205 	assertLess("1.0.0-alphz", "1.0.0-alphz0");
206 	assertLess("1.0.0-alphZ", "1.0.0-alpha");
207 }
208 
209 
210 /**
211 	Increments a given (partial) version number to the next higher version.
212 
213 	Prerelease and build metadata information is ignored. The given version
214 	can skip the minor and patch digits. If no digits are skipped, the next
215 	minor version will be selected. If the patch or minor versions are skipped,
216 	the next major version will be selected.
217 
218 	This function corresponds to the semantivs of the "~>" comparison operator's
219 	upper bound.
220 
221 	The semantics of this are the same as for the "approximate" version
222 	specifier from rubygems.
223 	(https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb)
224 
225 	See_Also: `expandVersion`
226 */
227 string bumpVersion(string ver)
228 pure {
229 	// Cut off metadata and prerelease information.
230 	auto mi = ver.indexOfAny("+-");
231 	if (mi > 0) ver = ver[0..mi];
232 	// Increment next to last version from a[.b[.c]].
233 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
234 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
235 	auto to_inc = splitted.length == 3? 1 : 0;
236 	splitted = splitted[0 .. to_inc+1];
237 	splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
238 	// Fill up to three compontents to make valid SemVer version.
239 	while (splitted.length < 3) splitted ~= "0";
240 	return splitted.join(".");
241 }
242 ///
243 unittest {
244 	assert("1.0.0" == bumpVersion("0"));
245 	assert("1.0.0" == bumpVersion("0.0"));
246 	assert("0.1.0" == bumpVersion("0.0.0"));
247 	assert("1.3.0" == bumpVersion("1.2.3"));
248 	assert("1.3.0" == bumpVersion("1.2.3+metadata"));
249 	assert("1.3.0" == bumpVersion("1.2.3-pre.release"));
250 	assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata"));
251 }
252 
253 /**
254 	Increments a given version number to the next incompatible version.
255 
256 	Prerelease and build metadata information is removed.
257 
258 	This implements the "^" comparison operator, which represents "nonbreaking semver compatibility."
259 	With 0.x.y releases, any release can break.
260 	With x.y.z releases, only major releases can break.
261 */
262 string bumpIncompatibleVersion(string ver)
263 pure {
264 	// Cut off metadata and prerelease information.
265 	auto mi = ver.indexOfAny("+-");
266 	if (mi > 0) ver = ver[0..mi];
267 	// Increment next to last version from a[.b[.c]].
268 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
269 	assert(splitted.length == 3, "Version corrupt: " ~ ver);
270 	if (splitted[0] == "0") splitted[2] = to!string(to!int(splitted[2]) + 1);
271 	else splitted = [to!string(to!int(splitted[0]) + 1), "0", "0"];
272 	return splitted.join(".");
273 }
274 ///
275 unittest {
276 	assert(bumpIncompatibleVersion("0.0.0") == "0.0.1");
277 	assert(bumpIncompatibleVersion("0.1.2") == "0.1.3");
278 	assert(bumpIncompatibleVersion("1.0.0") == "2.0.0");
279 	assert(bumpIncompatibleVersion("1.2.3") == "2.0.0");
280 	assert(bumpIncompatibleVersion("1.2.3+metadata") == "2.0.0");
281 	assert(bumpIncompatibleVersion("1.2.3-pre.release") == "2.0.0");
282 	assert(bumpIncompatibleVersion("1.2.3-pre.release+metadata") == "2.0.0");
283 }
284 
285 /**
286 	Takes a partial version and expands it to a valid SemVer version.
287 
288 	This function corresponds to the semantivs of the "~>" comparison operator's
289 	lower bound.
290 
291 	See_Also: `bumpVersion`
292 */
293 string expandVersion(string ver)
294 pure {
295 	auto mi = ver.indexOfAny("+-");
296 	auto sub = "";
297 	if (mi > 0) {
298 		sub = ver[mi..$];
299 		ver = ver[0..mi];
300 	}
301 	auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0
302 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
303 	while (splitted.length < 3) splitted ~= "0";
304 	return splitted.join(".") ~ sub;
305 }
306 ///
307 unittest {
308 	assert("1.0.0" == expandVersion("1"));
309 	assert("1.0.0" == expandVersion("1.0"));
310 	assert("1.0.0" == expandVersion("1.0.0"));
311 	// These are rather excotic variants...
312 	assert("1.0.0-pre.release" == expandVersion("1-pre.release"));
313 	assert("1.0.0+meta" == expandVersion("1+meta"));
314 	assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta"));
315 }
316 
317 private int compareIdentifier(ref string a, ref string b)
318 pure @nogc {
319 	bool anumber = true;
320 	bool bnumber = true;
321 	bool aempty = true, bempty = true;
322 	int res = 0;
323 	while (true) {
324 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
325 		if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false;
326 		if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false;
327 		a = a[1 .. $]; b = b[1 .. $];
328 		aempty = !a.length || a[0] == '.' || a[0] == '+';
329 		bempty = !b.length || b[0] == '.' || b[0] == '+';
330 		if (aempty || bempty) break;
331 	}
332 
333 	if (anumber && bnumber) {
334 		// the !empty value might be an indentifier instead of a number, but identifiers always have precedence
335 		if (aempty != bempty) return bempty - aempty;
336 		return res;
337 	} else {
338 		if (anumber && aempty) return -1;
339 		if (bnumber && bempty) return 1;
340 		// this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
341 		static assert('0' < 'a' && '0' < 'A');
342 		if (res != 0) return res;
343 		return bempty - aempty;
344 	}
345 }
346 
347 private int compareNumber(ref string a, ref string b)
348 pure @nogc {
349 	int res = 0;
350 	while (true) {
351 		if (a[0] != b[0] && res == 0) res = a[0] - b[0];
352 		a = a[1 .. $]; b = b[1 .. $];
353 		auto aempty = !a.length || (a[0] < '0' || a[0] > '9');
354 		auto bempty = !b.length || (b[0] < '0' || b[0] > '9');
355 		if (aempty != bempty) return bempty - aempty;
356 		if (aempty) return res;
357 	}
358 }
359 
360 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
361 pure @nogc {
362 	if (str.length == 0) return false;
363 	while (str.length) {
364 		auto end = str.indexOf('.');
365 		if (end < 0) end = str.length;
366 		if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
367 		if (end < str.length) str = str[end+1 .. $];
368 		else break;
369 	}
370 	return true;
371 }
372 
373 private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
374 pure @nogc {
375 	if (str.length < 1) return false;
376 
377 	bool numeric = true;
378 	foreach (ch; str) {
379 		switch (ch) {
380 			default: return false;
381 			case 'a': .. case 'z':
382 			case 'A': .. case 'Z':
383 			case '-':
384 				numeric = false;
385 				break;
386 			case '0': .. case '9':
387 				break;
388 		}
389 	}
390 
391 	if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
392 
393 	return true;
394 }
395 
396 private bool isValidNumber(string str)
397 pure @nogc {
398 	if (str.length < 1) return false;
399 	foreach (ch; str)
400 		if (ch < '0' || ch > '9')
401 			return false;
402 
403 	// don't allow leading zeros
404 	if (str[0] == '0' && str.length > 1) return false;
405 
406 	return true;
407 }
408 
409 private ptrdiff_t indexOfAny(string str, in char[] chars)
410 pure @nogc {
411 	ptrdiff_t ret = -1;
412 	foreach (ch; chars) {
413 		auto idx = str.indexOf(ch);
414 		if (idx >= 0 && (ret < 0 || idx < ret))
415 			ret = idx;
416 	}
417 	return ret;
418 }