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