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 @safe: 24 25 /** 26 Validates a version string according to the SemVer specification. 27 */ 28 bool isValidVersion(string ver) 29 pure @nogc { 30 // NOTE: this is not by spec, but to ensure sane input 31 if (ver.length > 256) return false; 32 33 // a 34 auto 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.indexOf('.'); 41 if (sepi < 0) return false; 42 if (!isValidNumber(ver[0 .. sepi])) return false; 43 ver = ver[sepi+1 .. $]; 44 45 // c 46 sepi = ver.indexOfAny("-+"); 47 if (sepi < 0) sepi = ver.length; 48 if (!isValidNumber(ver[0 .. sepi])) return false; 49 ver = ver[sepi .. $]; 50 51 // prerelease tail 52 if (ver.length > 0 && ver[0] == '-') { 53 ver = ver[1 .. $]; 54 sepi = ver.indexOf('+'); 55 if (sepi < 0) sepi = ver.length; 56 if (!isValidIdentifierChain(ver[0 .. sepi])) return false; 57 ver = ver[sepi .. $]; 58 } 59 60 // build tail 61 if (ver.length > 0 && ver[0] == '+') { 62 ver = ver[1 .. $]; 63 if (!isValidIdentifierChain(ver, true)) return false; 64 ver = null; 65 } 66 67 assert(ver.length == 0); 68 return true; 69 } 70 71 /// 72 unittest { 73 assert(isValidVersion("1.9.0")); 74 assert(isValidVersion("0.10.0")); 75 assert(!isValidVersion("01.9.0")); 76 assert(!isValidVersion("1.09.0")); 77 assert(!isValidVersion("1.9.00")); 78 assert(isValidVersion("1.0.0-alpha")); 79 assert(isValidVersion("1.0.0-alpha.1")); 80 assert(isValidVersion("1.0.0-0.3.7")); 81 assert(isValidVersion("1.0.0-x.7.z.92")); 82 assert(isValidVersion("1.0.0-x.7-z.92")); 83 assert(!isValidVersion("1.0.0-00.3.7")); 84 assert(!isValidVersion("1.0.0-0.03.7")); 85 assert(isValidVersion("1.0.0-alpha+001")); 86 assert(isValidVersion("1.0.0+20130313144700")); 87 assert(isValidVersion("1.0.0-beta+exp.sha.5114f85")); 88 assert(!isValidVersion(" 1.0.0")); 89 assert(!isValidVersion("1. 0.0")); 90 assert(!isValidVersion("1.0 .0")); 91 assert(!isValidVersion("1.0.0 ")); 92 assert(!isValidVersion("1.0.0-a_b")); 93 assert(!isValidVersion("1.0.0+")); 94 assert(!isValidVersion("1.0.0-")); 95 assert(!isValidVersion("1.0.0-+a")); 96 assert(!isValidVersion("1.0.0-a+")); 97 assert(!isValidVersion("1.0")); 98 assert(!isValidVersion("1.0-1.0")); 99 } 100 101 102 /** 103 Determines if a given valid SemVer version has a pre-release suffix. 104 */ 105 bool isPreReleaseVersion(string ver) pure @nogc 106 in { assert(isValidVersion(ver)); } 107 body { 108 foreach (i; 0 .. 2) { 109 auto di = ver.indexOf('.'); 110 assert(di > 0); 111 ver = ver[di+1 .. $]; 112 } 113 auto di = ver.indexOf('-'); 114 if (di < 0) return false; 115 return isValidNumber(ver[0 .. di]); 116 } 117 118 /// 119 unittest { 120 assert(isPreReleaseVersion("1.0.0-alpha")); 121 assert(isPreReleaseVersion("1.0.0-alpha+b1")); 122 assert(isPreReleaseVersion("0.9.0-beta.1")); 123 assert(!isPreReleaseVersion("0.9.0")); 124 assert(!isPreReleaseVersion("0.9.0+b1")); 125 } 126 127 /** 128 Compares the precedence of two SemVer version strings. 129 130 The version strings must be validated using `isValidVersion` before being 131 passed to this function. Note that the build meta data suffix (if any) is 132 being ignored when comparing version numbers. 133 134 Returns: 135 Returns a negative number if `a` is a lower version than `b`, `0` if they are 136 equal, and a positive number otherwise. 137 */ 138 int compareVersions(string a, string b) 139 pure @nogc { 140 // compare a.b.c numerically 141 if (auto ret = compareNumber(a, b)) return ret; 142 assert(a[0] == '.' && b[0] == '.'); 143 a = a[1 .. $]; b = b[1 .. $]; 144 if (auto ret = compareNumber(a, b)) return ret; 145 assert(a[0] == '.' && b[0] == '.'); 146 a = a[1 .. $]; b = b[1 .. $]; 147 if (auto ret = compareNumber(a, b)) return ret; 148 149 // give precedence to non-prerelease versions 150 bool apre = a.length > 0 && a[0] == '-'; 151 bool bpre = b.length > 0 && b[0] == '-'; 152 if (apre != bpre) return bpre - apre; 153 if (!apre) return 0; 154 155 // compare the prerelease tail lexicographically 156 do { 157 a = a[1 .. $]; b = b[1 .. $]; 158 if (auto ret = compareIdentifier(a, b)) return ret; 159 } while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+'); 160 161 // give longer prerelease tails precedence 162 bool aempty = a.length == 0 || a[0] == '+'; 163 bool bempty = b.length == 0 || b[0] == '+'; 164 if (aempty == bempty) { 165 assert(aempty); 166 return 0; 167 } 168 return bempty - aempty; 169 } 170 171 /// 172 unittest { 173 assert(compareVersions("1.0.0", "1.0.0") == 0); 174 assert(compareVersions("1.0.0+b1", "1.0.0+b2") == 0); 175 assert(compareVersions("1.0.0", "2.0.0") < 0); 176 assert(compareVersions("1.0.0-beta", "1.0.0") < 0); 177 assert(compareVersions("1.0.1", "1.0.0") > 0); 178 } 179 180 unittest { 181 void assertLess(string a, string b) { 182 assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b); 183 assert(compareVersions(b, a) > 0); 184 assert(compareVersions(a, a) == 0); 185 assert(compareVersions(b, b) == 0); 186 } 187 assertLess("1.0.0", "2.0.0"); 188 assertLess("2.0.0", "2.1.0"); 189 assertLess("2.1.0", "2.1.1"); 190 assertLess("1.0.0-alpha", "1.0.0"); 191 assertLess("1.0.0-alpha", "1.0.0-alpha.1"); 192 assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta"); 193 assertLess("1.0.0-alpha.beta", "1.0.0-beta"); 194 assertLess("1.0.0-beta", "1.0.0-beta.2"); 195 assertLess("1.0.0-beta.2", "1.0.0-beta.11"); 196 assertLess("1.0.0-beta.11", "1.0.0-rc.1"); 197 assertLess("1.0.0-rc.1", "1.0.0"); 198 assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0); 199 assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0); 200 assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0); 201 assertLess("2.0.0", "10.0.0"); 202 assertLess("1.0.0-2", "1.0.0-10"); 203 assertLess("1.0.0-99", "1.0.0-1a"); 204 assertLess("1.0.0-99", "1.0.0-a"); 205 assertLess("1.0.0-alpha", "1.0.0-alphb"); 206 assertLess("1.0.0-alphz", "1.0.0-alphz0"); 207 assertLess("1.0.0-alphZ", "1.0.0-alpha"); 208 } 209 210 211 /** 212 Increments a given (partial) version number to the next higher version. 213 214 Prerelease and build metadata information is ignored. The given version 215 can skip the minor and patch digits. If no digits are skipped, the next 216 minor version will be selected. If the patch or minor versions are skipped, 217 the next major version will be selected. 218 219 This function corresponds to the semantivs of the "~>" comparison operator's 220 upper bound. 221 222 The semantics of this are the same as for the "approximate" version 223 specifier from rubygems. 224 (https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb) 225 226 See_Also: `expandVersion` 227 */ 228 string bumpVersion(string ver) 229 pure { 230 // Cut off metadata and prerelease information. 231 auto mi = ver.indexOfAny("+-"); 232 if (mi > 0) ver = ver[0..mi]; 233 // Increment next to last version from a[.b[.c]]. 234 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 235 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 236 auto to_inc = splitted.length == 3? 1 : 0; 237 splitted = splitted[0 .. to_inc+1]; 238 splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1); 239 // Fill up to three compontents to make valid SemVer version. 240 while (splitted.length < 3) splitted ~= "0"; 241 return splitted.join("."); 242 } 243 /// 244 unittest { 245 assert("1.0.0" == bumpVersion("0")); 246 assert("1.0.0" == bumpVersion("0.0")); 247 assert("0.1.0" == bumpVersion("0.0.0")); 248 assert("1.3.0" == bumpVersion("1.2.3")); 249 assert("1.3.0" == bumpVersion("1.2.3+metadata")); 250 assert("1.3.0" == bumpVersion("1.2.3-pre.release")); 251 assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata")); 252 } 253 254 /** 255 Increments a given version number to the next incompatible version. 256 257 Prerelease and build metadata information is removed. 258 259 This implements the "^" comparison operator, which represents "nonbreaking semver compatibility." 260 With 0.x.y releases, any release can break. 261 With x.y.z releases, only major releases can break. 262 */ 263 string bumpIncompatibleVersion(string ver) 264 pure { 265 // Cut off metadata and prerelease information. 266 auto mi = ver.indexOfAny("+-"); 267 if (mi > 0) ver = ver[0..mi]; 268 // Increment next to last version from a[.b[.c]]. 269 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 270 assert(splitted.length == 3, "Version corrupt: " ~ ver); 271 if (splitted[0] == "0") splitted[2] = to!string(to!int(splitted[2]) + 1); 272 else splitted = [to!string(to!int(splitted[0]) + 1), "0", "0"]; 273 return splitted.join("."); 274 } 275 /// 276 unittest { 277 assert(bumpIncompatibleVersion("0.0.0") == "0.0.1"); 278 assert(bumpIncompatibleVersion("0.1.2") == "0.1.3"); 279 assert(bumpIncompatibleVersion("1.0.0") == "2.0.0"); 280 assert(bumpIncompatibleVersion("1.2.3") == "2.0.0"); 281 assert(bumpIncompatibleVersion("1.2.3+metadata") == "2.0.0"); 282 assert(bumpIncompatibleVersion("1.2.3-pre.release") == "2.0.0"); 283 assert(bumpIncompatibleVersion("1.2.3-pre.release+metadata") == "2.0.0"); 284 } 285 286 /** 287 Takes a partial version and expands it to a valid SemVer version. 288 289 This function corresponds to the semantivs of the "~>" comparison operator's 290 lower bound. 291 292 See_Also: `bumpVersion` 293 */ 294 string expandVersion(string ver) 295 pure { 296 auto mi = ver.indexOfAny("+-"); 297 auto sub = ""; 298 if (mi > 0) { 299 sub = ver[mi..$]; 300 ver = ver[0..mi]; 301 } 302 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 303 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 304 while (splitted.length < 3) splitted ~= "0"; 305 return splitted.join(".") ~ sub; 306 } 307 /// 308 unittest { 309 assert("1.0.0" == expandVersion("1")); 310 assert("1.0.0" == expandVersion("1.0")); 311 assert("1.0.0" == expandVersion("1.0.0")); 312 // These are rather excotic variants... 313 assert("1.0.0-pre.release" == expandVersion("1-pre.release")); 314 assert("1.0.0+meta" == expandVersion("1+meta")); 315 assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta")); 316 } 317 318 private int compareIdentifier(ref string a, ref string b) 319 pure @nogc { 320 bool anumber = true; 321 bool bnumber = true; 322 bool aempty = true, bempty = true; 323 int res = 0; 324 while (true) { 325 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 326 if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false; 327 if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false; 328 a = a[1 .. $]; b = b[1 .. $]; 329 aempty = !a.length || a[0] == '.' || a[0] == '+'; 330 bempty = !b.length || b[0] == '.' || b[0] == '+'; 331 if (aempty || bempty) break; 332 } 333 334 if (anumber && bnumber) { 335 // the !empty value might be an indentifier instead of a number, but identifiers always have precedence 336 if (aempty != bempty) return bempty - aempty; 337 return res; 338 } else { 339 if (anumber && aempty) return -1; 340 if (bnumber && bempty) return 1; 341 // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)! 342 static assert('0' < 'a' && '0' < 'A'); 343 if (res != 0) return res; 344 return bempty - aempty; 345 } 346 } 347 348 private int compareNumber(ref string a, ref string b) 349 pure @nogc { 350 int res = 0; 351 while (true) { 352 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 353 a = a[1 .. $]; b = b[1 .. $]; 354 auto aempty = !a.length || (a[0] < '0' || a[0] > '9'); 355 auto bempty = !b.length || (b[0] < '0' || b[0] > '9'); 356 if (aempty != bempty) return bempty - aempty; 357 if (aempty) return res; 358 } 359 } 360 361 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false) 362 pure @nogc { 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(string str, bool allow_leading_zeros = false) 375 pure @nogc { 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 { 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(string str, in char[] chars) 411 pure @nogc { 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 }