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 
24 /**
25 	Validates a version string according to the SemVer specification.
26 */
27 bool isValidVersion(string ver)
28 {
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)
105 in { assert(isValidVersion(ver)); }
106 body {
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 {
139 	// compare a.b.c numerically
140 	if (auto ret = compareNumber(a, b)) return ret;
141 	assert(a[0] == '.' && b[0] == '.');
142 	a.popFront(); b.popFront();
143 	if (auto ret = compareNumber(a, b)) return ret;
144 	assert(a[0] == '.' && b[0] == '.');
145 	a.popFront(); b.popFront();
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.popFront(); b.popFront();
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 	// Cut off metadata and prerelease information.
229 	auto mi = ver.indexOfAny("+-");
230 	if (mi > 0) ver = ver[0..mi];
231 	// Increment next to last version from a[.b[.c]].
232 	auto splitted = split(ver, ".");
233 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
234 	auto to_inc = splitted.length == 3? 1 : 0;
235 	splitted = splitted[0 .. to_inc+1];
236 	splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
237 	// Fill up to three compontents to make valid SemVer version.
238 	while (splitted.length < 3) splitted ~= "0";
239 	return splitted.join(".");
240 }
241 ///
242 unittest {
243 	assert("1.0.0" == bumpVersion("0"));
244 	assert("1.0.0" == bumpVersion("0.0"));
245 	assert("0.1.0" == bumpVersion("0.0.0"));
246 	assert("1.3.0" == bumpVersion("1.2.3"));
247 	assert("1.3.0" == bumpVersion("1.2.3+metadata"));
248 	assert("1.3.0" == bumpVersion("1.2.3-pre.release"));
249 	assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata"));
250 }
251 
252 /**
253 	Takes a partial version and expands it to a valid SemVer version.
254 	
255 	This function corresponds to the semantivs of the "~>" comparison operator's
256 	lower bound.
257 
258 	See_Also: `bumpVersion`
259 */
260 string expandVersion(string ver) {
261 	auto mi = ver.indexOfAny("+-");
262 	auto sub = "";
263 	if (mi > 0) {
264 		sub = ver[mi..$];
265 		ver = ver[0..mi];
266 	}
267 	auto splitted = split(ver, ".");
268 	assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
269 	while (splitted.length < 3) splitted ~= "0";
270 	return splitted.join(".") ~ sub;
271 }
272 ///
273 unittest {
274 	assert("1.0.0" == expandVersion("1"));
275 	assert("1.0.0" == expandVersion("1.0"));
276 	assert("1.0.0" == expandVersion("1.0.0"));
277 	// These are rather excotic variants...
278 	assert("1.0.0-pre.release" == expandVersion("1-pre.release"));
279 	assert("1.0.0+meta" == expandVersion("1+meta"));
280 	assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta"));
281 }
282 
283 private int compareIdentifier(ref string a, ref string b)
284 {
285 	bool anumber = true;
286 	bool bnumber = true;
287 	bool aempty = true, bempty = true;
288 	int res = 0;
289 	while (true) {
290 		if (a.front != b.front && res == 0) res = a.front - b.front;
291 		if (anumber && (a.front < '0' || a.front > '9')) anumber = false;
292 		if (bnumber && (b.front < '0' || b.front > '9')) bnumber = false;
293 		a.popFront(); b.popFront();
294 		aempty = a.empty || a.front == '.' || a.front == '+';
295 		bempty = b.empty || b.front == '.' || b.front == '+';
296 		if (aempty || bempty) break;
297 	}
298 
299 	if (anumber && bnumber) {
300 		// the !empty value might be an indentifier instead of a number, but identifiers always have precedence
301 		if (aempty != bempty) return bempty - aempty;
302 		return res;
303 	} else {
304 		if (anumber && aempty) return -1;
305 		if (bnumber && bempty) return 1;
306 		// this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
307 		static assert('0' < 'a' && '0' < 'A');
308 		if (res != 0) return res;
309 		return bempty - aempty;
310 	}
311 }
312 
313 private int compareNumber(ref string a, ref string b)
314 {
315 	int res = 0;
316 	while (true) {
317 		if (a.front != b.front && res == 0) res = a.front - b.front;
318 		a.popFront(); b.popFront();
319 		auto aempty = a.empty || (a.front < '0' || a.front > '9');
320 		auto bempty = b.empty || (b.front < '0' || b.front > '9');
321 		if (aempty != bempty) return bempty - aempty;
322 		if (aempty) return res;
323 	}
324 }
325 
326 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
327 {
328 	if (str.length == 0) return false;
329 	while (str.length) {
330 		auto end = str.indexOf('.');
331 		if (end < 0) end = str.length;
332 		if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
333 		if (end < str.length) str = str[end+1 .. $];
334 		else break;
335 	}
336 	return true;
337 }
338 
339 private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
340 {
341 	if (str.length < 1) return false;
342 
343 	bool numeric = true;
344 	foreach (ch; str) {
345 		switch (ch) {
346 			default: return false;
347 			case 'a': .. case 'z':
348 			case 'A': .. case 'Z':
349 			case '-':
350 				numeric = false;
351 				break;
352 			case '0': .. case '9':
353 				break;
354 		}
355 	}
356 
357 	if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
358 
359 	return true;
360 }
361 
362 private bool isValidNumber(string str)
363 {
364 	if (str.length < 1) return false;
365 	foreach (ch; str)
366 		if (ch < '0' || ch > '9')
367 			return false;
368 
369 	// don't allow leading zeros
370 	if (str[0] == '0' && str.length > 1) return false;
371 
372 	return true;
373 }
374 
375 private sizediff_t indexOfAny(string str, in char[] chars)
376 {
377 	sizediff_t ret = -1;
378 	foreach (ch; chars) {
379 		auto idx = str.indexOf(ch);
380 		if (idx >= 0 && (ret < 0 || idx < ret))
381 			ret = idx;
382 	}
383 	return ret;
384 }