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 
13 /*
14 	General format of SemVer: a.b.c[-x.y...][+x.y...]
15 	a/b/c must be integer numbers with no leading zeros
16 	x/y/... must be either numbers or identifiers containing only ASCII alphabetic characters or hyphens
17 */
18 
19 /**
20 	Validates a version string according to the SemVer specification.
21 */
22 bool isValidVersion(string ver)
23 {
24 	// NOTE: this is not by spec, but to ensure sane input
25 	if (ver.length > 256) return false;
26 
27 	// a
28 	auto sepi = ver.indexOf('.');
29 	if (sepi < 0) return false;
30 	if (!isValidNumber(ver[0 .. sepi])) return false;
31 	ver = ver[sepi+1 .. $];
32 
33 	// c
34 	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.indexOfAny("-+");
41 	if (sepi < 0) sepi = ver.length;
42 	if (!isValidNumber(ver[0 .. sepi])) return false;
43 	ver = ver[sepi .. $];
44 
45 	// prerelease tail
46 	if (ver.length > 0 && ver[0] == '-') {
47 		ver = ver[1 .. $];
48 		sepi = ver.indexOf('+');
49 		if (sepi < 0) sepi = ver.length;
50 		if (!isValidIdentifierChain(ver[0 .. sepi])) return false;
51 		ver = ver[sepi .. $];
52 	}
53 
54 	// build tail
55 	if (ver.length > 0 && ver[0] == '+') {
56 		ver = ver[1 .. $];
57 		if (!isValidIdentifierChain(ver, true)) return false;
58 		ver = null;
59 	}
60 
61 	assert(ver.length == 0);
62 	return true;
63 }
64 
65 unittest {
66 	assert(isValidVersion("1.9.0"));
67 	assert(isValidVersion("0.10.0"));
68 	assert(!isValidVersion("01.9.0"));
69 	assert(!isValidVersion("1.09.0"));
70 	assert(!isValidVersion("1.9.00"));
71 	assert(isValidVersion("1.0.0-alpha"));
72 	assert(isValidVersion("1.0.0-alpha.1"));
73 	assert(isValidVersion("1.0.0-0.3.7"));
74 	assert(isValidVersion("1.0.0-x.7.z.92"));
75 	assert(isValidVersion("1.0.0-x.7-z.92"));
76 	assert(!isValidVersion("1.0.0-00.3.7"));
77 	assert(!isValidVersion("1.0.0-0.03.7"));
78 	assert(isValidVersion("1.0.0-alpha+001"));
79 	assert(isValidVersion("1.0.0+20130313144700"));
80 	assert(isValidVersion("1.0.0-beta+exp.sha.5114f85"));
81 	assert(!isValidVersion(" 1.0.0"));
82 	assert(!isValidVersion("1. 0.0"));
83 	assert(!isValidVersion("1.0 .0"));
84 	assert(!isValidVersion("1.0.0 "));
85 	assert(!isValidVersion("1.0.0-a_b"));
86 	assert(!isValidVersion("1.0.0+"));
87 	assert(!isValidVersion("1.0.0-"));
88 	assert(!isValidVersion("1.0.0-+a"));
89 	assert(!isValidVersion("1.0.0-a+"));
90 	assert(!isValidVersion("1.0"));
91 	assert(!isValidVersion("1.0-1.0"));
92 }
93 
94 bool isPreReleaseVersion(string ver)
95 in { assert(isValidVersion(ver)); }
96 body {
97 	foreach (i; 0 .. 2) {
98 		auto di = ver.indexOf('.');
99 		assert(di > 0);
100 		ver = ver[di+1 .. $];
101 	}
102 	auto di = ver.indexOf('-');
103 	if (di < 0) return false;
104 	return isValidNumber(ver[0 .. di]);
105 }
106 
107 /**
108 	Compares the precedence of two SemVer version strings.
109 
110 	The version strings must be validated using isValidVersion() before being
111 	passed to this function.
112 */
113 int compareVersions(string a, string b)
114 {
115 	// compare a.b.c numerically
116 	if (auto ret = compareNumber(a, b)) return ret;
117 	assert(a[0] == '.' && b[0] == '.');
118 	a.popFront(); b.popFront();
119 	if (auto ret = compareNumber(a, b)) return ret;
120 	assert(a[0] == '.' && b[0] == '.');
121 	a.popFront(); b.popFront();
122 	if (auto ret = compareNumber(a, b)) return ret;
123 
124 	// give precedence to non-prerelease versions
125 	bool apre = a.length > 0 && a[0] == '-';
126 	bool bpre = b.length > 0 && b[0] == '-';
127 	if (apre != bpre) return bpre - apre;
128 	if (!apre) return 0;
129 
130 	// compare the prerelease tail lexicographically
131 	do {
132 		a.popFront(); b.popFront();
133 		if (auto ret = compareIdentifier(a, b)) return ret;
134 	} while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+');
135 
136 	// give longer prerelease tails precedence
137 	bool aempty = a.length == 0 || a[0] == '+';
138 	bool bempty = b.length == 0 || b[0] == '+';
139 	if (aempty == bempty) {
140 		assert(aempty);
141 		return 0;
142 	}
143 	return bempty - aempty;
144 }
145 
146 unittest {
147 	void assertLess(string a, string b) {
148 		assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b);
149 		assert(compareVersions(b, a) > 0);
150 		assert(compareVersions(a, a) == 0);
151 		assert(compareVersions(b, b) == 0);
152 	}
153 	assertLess("1.0.0", "2.0.0");
154 	assertLess("2.0.0", "2.1.0");
155 	assertLess("2.1.0", "2.1.1");
156 	assertLess("1.0.0-alpha", "1.0.0");
157 	assertLess("1.0.0-alpha", "1.0.0-alpha.1");
158 	assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta");
159 	assertLess("1.0.0-alpha.beta", "1.0.0-beta");
160 	assertLess("1.0.0-beta", "1.0.0-beta.2");
161 	assertLess("1.0.0-beta.2", "1.0.0-beta.11");
162 	assertLess("1.0.0-beta.11", "1.0.0-rc.1");
163 	assertLess("1.0.0-rc.1", "1.0.0");
164 	assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0);
165 	assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0);
166 	assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0);
167 	assertLess("2.0.0", "10.0.0");
168 	assertLess("1.0.0-2", "1.0.0-10");
169 	assertLess("1.0.0-99", "1.0.0-1a");
170 	assertLess("1.0.0-99", "1.0.0-a");
171 }
172 
173 
174 private int compareIdentifier(ref string a, ref string b)
175 {
176 	bool anumber = true;
177 	bool bnumber = true;
178 	bool aempty = true, bempty = true;
179 	int res = 0;
180 	while (true) {
181 		if (a.front != b.front && res == 0) res = a.front - b.front;
182 		if (anumber && (a.front < '0' || a.front > '9')) anumber = false;
183 		if (bnumber && (b.front < '0' || b.front > '9')) bnumber = false;
184 		a.popFront(); b.popFront();
185 		aempty = a.empty || a.front == '.' || a.front == '+';
186 		bempty = b.empty || b.front == '.' || b.front == '+';
187 		if (aempty || bempty) break;
188 	}
189 
190 	if (anumber && bnumber) {
191 		// the !empty value might be an indentifier instead of a number, but identifiers always have precedence
192 		if (aempty != bempty) return bempty - aempty;
193 		return res;
194 	} else {
195 		if (anumber && aempty) return -1;
196 		if (bnumber && bempty) return 1;
197 		// this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
198 		static assert('0' < 'a' && '0' < 'A');
199 		if (res != 0) return res;
200 		return bempty - aempty;
201 	}
202 }
203 
204 private int compareNumber(ref string a, ref string b)
205 {
206 	int res = 0;
207 	while (true) {
208 		if (a.front != b.front && res == 0) res = a.front - b.front;
209 		a.popFront(); b.popFront();
210 		auto aempty = a.empty || (a.front < '0' || a.front > '9');
211 		auto bempty = b.empty || (b.front < '0' || b.front > '9');
212 		if (aempty != bempty) return bempty - aempty;
213 		if (aempty) return res;
214 	}
215 }
216 
217 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
218 {
219 	if (str.length == 0) return false;
220 	while (str.length) {
221 		auto end = str.indexOf('.');
222 		if (end < 0) end = str.length;
223 		if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
224 		if (end < str.length) str = str[end+1 .. $];
225 		else break;
226 	}
227 	return true;
228 }
229 
230 private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
231 {
232 	if (str.length < 1) return false;
233 
234 	bool numeric = true;
235 	foreach (ch; str) {
236 		switch (ch) {
237 			default: return false;
238 			case 'a': .. case 'z':
239 			case 'A': .. case 'Z':
240 			case '-':
241 				numeric = false;
242 				break;
243 			case '0': .. case '9':
244 				break;
245 		}
246 	}
247 
248 	if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
249 
250 	return true;
251 }
252 
253 private bool isValidNumber(string str)
254 {
255 	if (str.length < 1) return false;
256 	foreach (ch; str)
257 		if (ch < '0' || ch > '9')
258 			return false;
259 
260 	// don't allow leading zeros
261 	if (str[0] == '0' && str.length > 1) return false;
262 
263 	return true;
264 }
265 
266 private sizediff_t indexOfAny(string str, in char[] chars)
267 {
268 	sizediff_t ret = -1;
269 	foreach (ch; chars) {
270 		auto idx = str.indexOf(ch);
271 		if (idx >= 0 && (ret < 0 || idx < ret))
272 			ret = idx;
273 	}
274 	return ret;
275 }