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(string ver) 28 pure @nogc { 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) pure @nogc 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(string a, string b) 138 pure @nogc { 139 // compare a.b.c numerically 140 if (auto ret = compareNumber(a, b)) return ret; 141 assert(a[0] == '.' && b[0] == '.'); 142 a = a[1 .. $]; b = b[1 .. $]; 143 if (auto ret = compareNumber(a, b)) return ret; 144 assert(a[0] == '.' && b[0] == '.'); 145 a = a[1 .. $]; b = b[1 .. $]; 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 = a[1 .. $]; b = b[1 .. $]; 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 pure { 229 // Cut off metadata and prerelease information. 230 auto mi = ver.indexOfAny("+-"); 231 if (mi > 0) ver = ver[0..mi]; 232 // Increment next to last version from a[.b[.c]]. 233 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 234 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 235 auto to_inc = splitted.length == 3? 1 : 0; 236 splitted = splitted[0 .. to_inc+1]; 237 splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1); 238 // Fill up to three compontents to make valid SemVer version. 239 while (splitted.length < 3) splitted ~= "0"; 240 return splitted.join("."); 241 } 242 /// 243 unittest { 244 assert("1.0.0" == bumpVersion("0")); 245 assert("1.0.0" == bumpVersion("0.0")); 246 assert("0.1.0" == bumpVersion("0.0.0")); 247 assert("1.3.0" == bumpVersion("1.2.3")); 248 assert("1.3.0" == bumpVersion("1.2.3+metadata")); 249 assert("1.3.0" == bumpVersion("1.2.3-pre.release")); 250 assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata")); 251 } 252 253 /** 254 Increments a given version number to the next incompatible version. 255 256 Prerelease and build metadata information is removed. 257 258 This implements the "^" comparison operator, which represents "nonbreaking semver compatibility." 259 With 0.x.y releases, any release can break. 260 With x.y.z releases, only major releases can break. 261 */ 262 string bumpIncompatibleVersion(string ver) 263 pure { 264 // Cut off metadata and prerelease information. 265 auto mi = ver.indexOfAny("+-"); 266 if (mi > 0) ver = ver[0..mi]; 267 // Increment next to last version from a[.b[.c]]. 268 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 269 assert(splitted.length == 3, "Version corrupt: " ~ ver); 270 if (splitted[0] == "0") splitted[2] = to!string(to!int(splitted[2]) + 1); 271 else splitted = [to!string(to!int(splitted[0]) + 1), "0", "0"]; 272 return splitted.join("."); 273 } 274 /// 275 unittest { 276 assert(bumpIncompatibleVersion("0.0.0") == "0.0.1"); 277 assert(bumpIncompatibleVersion("0.1.2") == "0.1.3"); 278 assert(bumpIncompatibleVersion("1.0.0") == "2.0.0"); 279 assert(bumpIncompatibleVersion("1.2.3") == "2.0.0"); 280 assert(bumpIncompatibleVersion("1.2.3+metadata") == "2.0.0"); 281 assert(bumpIncompatibleVersion("1.2.3-pre.release") == "2.0.0"); 282 assert(bumpIncompatibleVersion("1.2.3-pre.release+metadata") == "2.0.0"); 283 } 284 285 /** 286 Takes a partial version and expands it to a valid SemVer version. 287 288 This function corresponds to the semantivs of the "~>" comparison operator's 289 lower bound. 290 291 See_Also: `bumpVersion` 292 */ 293 string expandVersion(string ver) 294 pure { 295 auto mi = ver.indexOfAny("+-"); 296 auto sub = ""; 297 if (mi > 0) { 298 sub = ver[mi..$]; 299 ver = ver[0..mi]; 300 } 301 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 302 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 303 while (splitted.length < 3) splitted ~= "0"; 304 return splitted.join(".") ~ sub; 305 } 306 /// 307 unittest { 308 assert("1.0.0" == expandVersion("1")); 309 assert("1.0.0" == expandVersion("1.0")); 310 assert("1.0.0" == expandVersion("1.0.0")); 311 // These are rather excotic variants... 312 assert("1.0.0-pre.release" == expandVersion("1-pre.release")); 313 assert("1.0.0+meta" == expandVersion("1+meta")); 314 assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta")); 315 } 316 317 private int compareIdentifier(ref string a, ref string b) 318 pure @nogc { 319 bool anumber = true; 320 bool bnumber = true; 321 bool aempty = true, bempty = true; 322 int res = 0; 323 while (true) { 324 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 325 if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false; 326 if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false; 327 a = a[1 .. $]; b = b[1 .. $]; 328 aempty = !a.length || a[0] == '.' || a[0] == '+'; 329 bempty = !b.length || b[0] == '.' || b[0] == '+'; 330 if (aempty || bempty) break; 331 } 332 333 if (anumber && bnumber) { 334 // the !empty value might be an indentifier instead of a number, but identifiers always have precedence 335 if (aempty != bempty) return bempty - aempty; 336 return res; 337 } else { 338 if (anumber && aempty) return -1; 339 if (bnumber && bempty) return 1; 340 // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)! 341 static assert('0' < 'a' && '0' < 'A'); 342 if (res != 0) return res; 343 return bempty - aempty; 344 } 345 } 346 347 private int compareNumber(ref string a, ref string b) 348 pure @nogc { 349 int res = 0; 350 while (true) { 351 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 352 a = a[1 .. $]; b = b[1 .. $]; 353 auto aempty = !a.length || (a[0] < '0' || a[0] > '9'); 354 auto bempty = !b.length || (b[0] < '0' || b[0] > '9'); 355 if (aempty != bempty) return bempty - aempty; 356 if (aempty) return res; 357 } 358 } 359 360 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false) 361 pure @nogc { 362 if (str.length == 0) return false; 363 while (str.length) { 364 auto end = str.indexOf('.'); 365 if (end < 0) end = str.length; 366 if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false; 367 if (end < str.length) str = str[end+1 .. $]; 368 else break; 369 } 370 return true; 371 } 372 373 private bool isValidIdentifier(string str, bool allow_leading_zeros = false) 374 pure @nogc { 375 if (str.length < 1) return false; 376 377 bool numeric = true; 378 foreach (ch; str) { 379 switch (ch) { 380 default: return false; 381 case 'a': .. case 'z': 382 case 'A': .. case 'Z': 383 case '-': 384 numeric = false; 385 break; 386 case '0': .. case '9': 387 break; 388 } 389 } 390 391 if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false; 392 393 return true; 394 } 395 396 private bool isValidNumber(string str) 397 pure @nogc { 398 if (str.length < 1) return false; 399 foreach (ch; str) 400 if (ch < '0' || ch > '9') 401 return false; 402 403 // don't allow leading zeros 404 if (str[0] == '0' && str.length > 1) return false; 405 406 return true; 407 } 408 409 private ptrdiff_t indexOfAny(string str, in char[] chars) 410 pure @nogc { 411 ptrdiff_t ret = -1; 412 foreach (ch; chars) { 413 auto idx = str.indexOf(ch); 414 if (idx >= 0 && (ret < 0 || idx < ret)) 415 ret = idx; 416 } 417 return ret; 418 }