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 }