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