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 Takes a partial version and expands it to a valid SemVer version. 256 257 This function corresponds to the semantivs of the "~>" comparison operator's 258 lower bound. 259 260 See_Also: `bumpVersion` 261 */ 262 string expandVersion(string ver) 263 pure { 264 auto mi = ver.indexOfAny("+-"); 265 auto sub = ""; 266 if (mi > 0) { 267 sub = ver[mi..$]; 268 ver = ver[0..mi]; 269 } 270 auto splitted = () @trusted { return split(ver, "."); } (); // DMD 2.065.0 271 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 272 while (splitted.length < 3) splitted ~= "0"; 273 return splitted.join(".") ~ sub; 274 } 275 /// 276 unittest { 277 assert("1.0.0" == expandVersion("1")); 278 assert("1.0.0" == expandVersion("1.0")); 279 assert("1.0.0" == expandVersion("1.0.0")); 280 // These are rather excotic variants... 281 assert("1.0.0-pre.release" == expandVersion("1-pre.release")); 282 assert("1.0.0+meta" == expandVersion("1+meta")); 283 assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta")); 284 } 285 286 private int compareIdentifier(ref string a, ref string b) 287 pure @nogc { 288 bool anumber = true; 289 bool bnumber = true; 290 bool aempty = true, bempty = true; 291 int res = 0; 292 while (true) { 293 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 294 if (anumber && (a[0] < '0' || a[0] > '9')) anumber = false; 295 if (bnumber && (b[0] < '0' || b[0] > '9')) bnumber = false; 296 a = a[1 .. $]; b = b[1 .. $]; 297 aempty = !a.length || a[0] == '.' || a[0] == '+'; 298 bempty = !b.length || b[0] == '.' || b[0] == '+'; 299 if (aempty || bempty) break; 300 } 301 302 if (anumber && bnumber) { 303 // the !empty value might be an indentifier instead of a number, but identifiers always have precedence 304 if (aempty != bempty) return bempty - aempty; 305 return res; 306 } else { 307 if (anumber && aempty) return -1; 308 if (bnumber && bempty) return 1; 309 // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)! 310 static assert('0' < 'a' && '0' < 'A'); 311 if (res != 0) return res; 312 return bempty - aempty; 313 } 314 } 315 316 private int compareNumber(ref string a, ref string b) 317 pure @nogc { 318 int res = 0; 319 while (true) { 320 if (a[0] != b[0] && res == 0) res = a[0] - b[0]; 321 a = a[1 .. $]; b = b[1 .. $]; 322 auto aempty = !a.length || (a[0] < '0' || a[0] > '9'); 323 auto bempty = !b.length || (b[0] < '0' || b[0] > '9'); 324 if (aempty != bempty) return bempty - aempty; 325 if (aempty) return res; 326 } 327 } 328 329 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false) 330 pure @nogc { 331 if (str.length == 0) return false; 332 while (str.length) { 333 auto end = str.indexOf('.'); 334 if (end < 0) end = str.length; 335 if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false; 336 if (end < str.length) str = str[end+1 .. $]; 337 else break; 338 } 339 return true; 340 } 341 342 private bool isValidIdentifier(string str, bool allow_leading_zeros = false) 343 pure @nogc { 344 if (str.length < 1) return false; 345 346 bool numeric = true; 347 foreach (ch; str) { 348 switch (ch) { 349 default: return false; 350 case 'a': .. case 'z': 351 case 'A': .. case 'Z': 352 case '-': 353 numeric = false; 354 break; 355 case '0': .. case '9': 356 break; 357 } 358 } 359 360 if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false; 361 362 return true; 363 } 364 365 private bool isValidNumber(string str) 366 pure @nogc { 367 if (str.length < 1) return false; 368 foreach (ch; str) 369 if (ch < '0' || ch > '9') 370 return false; 371 372 // don't allow leading zeros 373 if (str[0] == '0' && str.length > 1) return false; 374 375 return true; 376 } 377 378 private sizediff_t indexOfAny(string str, in char[] chars) 379 pure @nogc { 380 sizediff_t ret = -1; 381 foreach (ch; chars) { 382 auto idx = str.indexOf(ch); 383 if (idx >= 0 && (ret < 0 || idx < ret)) 384 ret = idx; 385 } 386 return ret; 387 }