1 /** 2 Implementes version validation and comparison according to the semantic versioning specification. 3 4 Copyright: © 2013 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module dub.semver; 9 10 import std.range; 11 import std..string; 12 import std.algorithm : max; 13 import std.conv; 14 15 /* 16 General format of SemVer: a.b.c[-x.y...][+x.y...] 17 a/b/c must be integer numbers with no leading zeros 18 x/y/... must be either numbers or identifiers containing only ASCII alphabetic characters or hyphens 19 */ 20 21 /** 22 Validates a version string according to the SemVer specification. 23 */ 24 bool isValidVersion(string ver) 25 { 26 // NOTE: this is not by spec, but to ensure sane input 27 if (ver.length > 256) return false; 28 29 // a 30 auto sepi = ver.indexOf('.'); 31 if (sepi < 0) return false; 32 if (!isValidNumber(ver[0 .. sepi])) return false; 33 ver = ver[sepi+1 .. $]; 34 35 // c 36 sepi = ver.indexOf('.'); 37 if (sepi < 0) return false; 38 if (!isValidNumber(ver[0 .. sepi])) return false; 39 ver = ver[sepi+1 .. $]; 40 41 // c 42 sepi = ver.indexOfAny("-+"); 43 if (sepi < 0) sepi = ver.length; 44 if (!isValidNumber(ver[0 .. sepi])) return false; 45 ver = ver[sepi .. $]; 46 47 // prerelease tail 48 if (ver.length > 0 && ver[0] == '-') { 49 ver = ver[1 .. $]; 50 sepi = ver.indexOf('+'); 51 if (sepi < 0) sepi = ver.length; 52 if (!isValidIdentifierChain(ver[0 .. sepi])) return false; 53 ver = ver[sepi .. $]; 54 } 55 56 // build tail 57 if (ver.length > 0 && ver[0] == '+') { 58 ver = ver[1 .. $]; 59 if (!isValidIdentifierChain(ver, true)) return false; 60 ver = null; 61 } 62 63 assert(ver.length == 0); 64 return true; 65 } 66 67 unittest { 68 assert(isValidVersion("1.9.0")); 69 assert(isValidVersion("0.10.0")); 70 assert(!isValidVersion("01.9.0")); 71 assert(!isValidVersion("1.09.0")); 72 assert(!isValidVersion("1.9.00")); 73 assert(isValidVersion("1.0.0-alpha")); 74 assert(isValidVersion("1.0.0-alpha.1")); 75 assert(isValidVersion("1.0.0-0.3.7")); 76 assert(isValidVersion("1.0.0-x.7.z.92")); 77 assert(isValidVersion("1.0.0-x.7-z.92")); 78 assert(!isValidVersion("1.0.0-00.3.7")); 79 assert(!isValidVersion("1.0.0-0.03.7")); 80 assert(isValidVersion("1.0.0-alpha+001")); 81 assert(isValidVersion("1.0.0+20130313144700")); 82 assert(isValidVersion("1.0.0-beta+exp.sha.5114f85")); 83 assert(!isValidVersion(" 1.0.0")); 84 assert(!isValidVersion("1. 0.0")); 85 assert(!isValidVersion("1.0 .0")); 86 assert(!isValidVersion("1.0.0 ")); 87 assert(!isValidVersion("1.0.0-a_b")); 88 assert(!isValidVersion("1.0.0+")); 89 assert(!isValidVersion("1.0.0-")); 90 assert(!isValidVersion("1.0.0-+a")); 91 assert(!isValidVersion("1.0.0-a+")); 92 assert(!isValidVersion("1.0")); 93 assert(!isValidVersion("1.0-1.0")); 94 } 95 96 bool isPreReleaseVersion(string ver) 97 in { assert(isValidVersion(ver)); } 98 body { 99 foreach (i; 0 .. 2) { 100 auto di = ver.indexOf('.'); 101 assert(di > 0); 102 ver = ver[di+1 .. $]; 103 } 104 auto di = ver.indexOf('-'); 105 if (di < 0) return false; 106 return isValidNumber(ver[0 .. di]); 107 } 108 109 /** 110 Compares the precedence of two SemVer version strings. 111 112 The version strings must be validated using isValidVersion() before being 113 passed to this function. 114 */ 115 int compareVersions(string a, string b) 116 { 117 // compare a.b.c numerically 118 if (auto ret = compareNumber(a, b)) return ret; 119 assert(a[0] == '.' && b[0] == '.'); 120 a.popFront(); b.popFront(); 121 if (auto ret = compareNumber(a, b)) return ret; 122 assert(a[0] == '.' && b[0] == '.'); 123 a.popFront(); b.popFront(); 124 if (auto ret = compareNumber(a, b)) return ret; 125 126 // give precedence to non-prerelease versions 127 bool apre = a.length > 0 && a[0] == '-'; 128 bool bpre = b.length > 0 && b[0] == '-'; 129 if (apre != bpre) return bpre - apre; 130 if (!apre) return 0; 131 132 // compare the prerelease tail lexicographically 133 do { 134 a.popFront(); b.popFront(); 135 if (auto ret = compareIdentifier(a, b)) return ret; 136 } while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+'); 137 138 // give longer prerelease tails precedence 139 bool aempty = a.length == 0 || a[0] == '+'; 140 bool bempty = b.length == 0 || b[0] == '+'; 141 if (aempty == bempty) { 142 assert(aempty); 143 return 0; 144 } 145 return bempty - aempty; 146 } 147 148 unittest { 149 void assertLess(string a, string b) { 150 assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b); 151 assert(compareVersions(b, a) > 0); 152 assert(compareVersions(a, a) == 0); 153 assert(compareVersions(b, b) == 0); 154 } 155 assertLess("1.0.0", "2.0.0"); 156 assertLess("2.0.0", "2.1.0"); 157 assertLess("2.1.0", "2.1.1"); 158 assertLess("1.0.0-alpha", "1.0.0"); 159 assertLess("1.0.0-alpha", "1.0.0-alpha.1"); 160 assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta"); 161 assertLess("1.0.0-alpha.beta", "1.0.0-beta"); 162 assertLess("1.0.0-beta", "1.0.0-beta.2"); 163 assertLess("1.0.0-beta.2", "1.0.0-beta.11"); 164 assertLess("1.0.0-beta.11", "1.0.0-rc.1"); 165 assertLess("1.0.0-rc.1", "1.0.0"); 166 assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0); 167 assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0); 168 assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0); 169 assertLess("2.0.0", "10.0.0"); 170 assertLess("1.0.0-2", "1.0.0-10"); 171 assertLess("1.0.0-99", "1.0.0-1a"); 172 assertLess("1.0.0-99", "1.0.0-a"); 173 assertLess("1.0.0-alpha", "1.0.0-alphb"); 174 assertLess("1.0.0-alphz", "1.0.0-alphz0"); 175 assertLess("1.0.0-alphZ", "1.0.0-alpha"); 176 } 177 178 179 /** 180 Given version string, increments the next to last version number. 181 Prerelease and build metadata information is ignored. 182 @param ver Does not need to be a valid semver version. 183 @return Valid semver version 184 185 The semantics of this are the same as for the "approximate" version 186 specifier from rubygems. 187 (https://github.com/rubygems/rubygems/tree/81d806d818baeb5dcb6398ca631d772a003d078e/lib/rubygems/version.rb) 188 189 Examples: 190 1.5 -> 2.0 191 1.5.67 -> 1.6.0 192 1.5.67-a -> 1.6.0 193 */ 194 string bumpVersion(string ver) { 195 // Cut off metadata and prerelease information. 196 auto mi = ver.indexOfAny("+-"); 197 if (mi > 0) ver = ver[0..mi]; 198 // Increment next to last version from a[.b[.c]]. 199 auto splitted = split(ver, "."); 200 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 201 auto to_inc = splitted.length == 3? 1 : 0; 202 splitted = splitted[0 .. to_inc+1]; 203 splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1); 204 // Fill up to three compontents to make valid SemVer version. 205 while (splitted.length < 3) splitted ~= "0"; 206 return splitted.join("."); 207 } 208 209 unittest { 210 assert("1.0.0" == bumpVersion("0")); 211 assert("1.0.0" == bumpVersion("0.0")); 212 assert("0.1.0" == bumpVersion("0.0.0")); 213 assert("1.3.0" == bumpVersion("1.2.3")); 214 assert("1.3.0" == bumpVersion("1.2.3+metadata")); 215 assert("1.3.0" == bumpVersion("1.2.3-pre.release")); 216 assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata")); 217 } 218 219 /** 220 Takes a abbreviated version and expands it to a valid SemVer version. 221 E.g. "1.0" -> "1.0.0" 222 */ 223 string expandVersion(string ver) { 224 auto mi = ver.indexOfAny("+-"); 225 auto sub = ""; 226 if (mi > 0) { 227 sub = ver[mi..$]; 228 ver = ver[0..mi]; 229 } 230 auto splitted = split(ver, "."); 231 assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver); 232 while (splitted.length < 3) splitted ~= "0"; 233 return splitted.join(".") ~ sub; 234 } 235 236 unittest { 237 assert("1.0.0" == expandVersion("1")); 238 assert("1.0.0" == expandVersion("1.0")); 239 assert("1.0.0" == expandVersion("1.0.0")); 240 // These are rather excotic variants... 241 assert("1.0.0-pre.release" == expandVersion("1-pre.release")); 242 assert("1.0.0+meta" == expandVersion("1+meta")); 243 assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta")); 244 } 245 246 private int compareIdentifier(ref string a, ref string b) 247 { 248 bool anumber = true; 249 bool bnumber = true; 250 bool aempty = true, bempty = true; 251 int res = 0; 252 while (true) { 253 if (a.front != b.front && res == 0) res = a.front - b.front; 254 if (anumber && (a.front < '0' || a.front > '9')) anumber = false; 255 if (bnumber && (b.front < '0' || b.front > '9')) bnumber = false; 256 a.popFront(); b.popFront(); 257 aempty = a.empty || a.front == '.' || a.front == '+'; 258 bempty = b.empty || b.front == '.' || b.front == '+'; 259 if (aempty || bempty) break; 260 } 261 262 if (anumber && bnumber) { 263 // the !empty value might be an indentifier instead of a number, but identifiers always have precedence 264 if (aempty != bempty) return bempty - aempty; 265 return res; 266 } else { 267 if (anumber && aempty) return -1; 268 if (bnumber && bempty) return 1; 269 // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)! 270 static assert('0' < 'a' && '0' < 'A'); 271 if (res != 0) return res; 272 return bempty - aempty; 273 } 274 } 275 276 private int compareNumber(ref string a, ref string b) 277 { 278 int res = 0; 279 while (true) { 280 if (a.front != b.front && res == 0) res = a.front - b.front; 281 a.popFront(); b.popFront(); 282 auto aempty = a.empty || (a.front < '0' || a.front > '9'); 283 auto bempty = b.empty || (b.front < '0' || b.front > '9'); 284 if (aempty != bempty) return bempty - aempty; 285 if (aempty) return res; 286 } 287 } 288 289 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false) 290 { 291 if (str.length == 0) return false; 292 while (str.length) { 293 auto end = str.indexOf('.'); 294 if (end < 0) end = str.length; 295 if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false; 296 if (end < str.length) str = str[end+1 .. $]; 297 else break; 298 } 299 return true; 300 } 301 302 private bool isValidIdentifier(string str, bool allow_leading_zeros = false) 303 { 304 if (str.length < 1) return false; 305 306 bool numeric = true; 307 foreach (ch; str) { 308 switch (ch) { 309 default: return false; 310 case 'a': .. case 'z': 311 case 'A': .. case 'Z': 312 case '-': 313 numeric = false; 314 break; 315 case '0': .. case '9': 316 break; 317 } 318 } 319 320 if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false; 321 322 return true; 323 } 324 325 private bool isValidNumber(string str) 326 { 327 if (str.length < 1) return false; 328 foreach (ch; str) 329 if (ch < '0' || ch > '9') 330 return false; 331 332 // don't allow leading zeros 333 if (str[0] == '0' && str.length > 1) return false; 334 335 return true; 336 } 337 338 private sizediff_t indexOfAny(string str, in char[] chars) 339 { 340 sizediff_t ret = -1; 341 foreach (ch; chars) { 342 auto idx = str.indexOf(ch); 343 if (idx >= 0 && (ret < 0 || idx < ret)) 344 ret = idx; 345 } 346 return ret; 347 }