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 13 /* 14 General format of SemVer: a.b.c[-x.y...][+x.y...] 15 a/b/c must be integer numbers with no leading zeros 16 x/y/... must be either numbers or identifiers containing only ASCII alphabetic characters or hyphens 17 */ 18 19 /** 20 Validates a version string according to the SemVer specification. 21 */ 22 bool isValidVersion(string ver) 23 { 24 // NOTE: this is not by spec, but to ensure sane input 25 if (ver.length > 256) return false; 26 27 // a 28 auto sepi = ver.indexOf('.'); 29 if (sepi < 0) return false; 30 if (!isValidNumber(ver[0 .. sepi])) return false; 31 ver = ver[sepi+1 .. $]; 32 33 // c 34 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.indexOfAny("-+"); 41 if (sepi < 0) sepi = ver.length; 42 if (!isValidNumber(ver[0 .. sepi])) return false; 43 ver = ver[sepi .. $]; 44 45 // prerelease tail 46 if (ver.length > 0 && ver[0] == '-') { 47 ver = ver[1 .. $]; 48 sepi = ver.indexOf('+'); 49 if (sepi < 0) sepi = ver.length; 50 if (!isValidIdentifierChain(ver[0 .. sepi])) return false; 51 ver = ver[sepi .. $]; 52 } 53 54 // build tail 55 if (ver.length > 0 && ver[0] == '+') { 56 ver = ver[1 .. $]; 57 if (!isValidIdentifierChain(ver, true)) return false; 58 ver = null; 59 } 60 61 assert(ver.length == 0); 62 return true; 63 } 64 65 unittest { 66 assert(isValidVersion("1.9.0")); 67 assert(isValidVersion("0.10.0")); 68 assert(!isValidVersion("01.9.0")); 69 assert(!isValidVersion("1.09.0")); 70 assert(!isValidVersion("1.9.00")); 71 assert(isValidVersion("1.0.0-alpha")); 72 assert(isValidVersion("1.0.0-alpha.1")); 73 assert(isValidVersion("1.0.0-0.3.7")); 74 assert(isValidVersion("1.0.0-x.7.z.92")); 75 assert(isValidVersion("1.0.0-x.7-z.92")); 76 assert(!isValidVersion("1.0.0-00.3.7")); 77 assert(!isValidVersion("1.0.0-0.03.7")); 78 assert(isValidVersion("1.0.0-alpha+001")); 79 assert(isValidVersion("1.0.0+20130313144700")); 80 assert(isValidVersion("1.0.0-beta+exp.sha.5114f85")); 81 assert(!isValidVersion(" 1.0.0")); 82 assert(!isValidVersion("1. 0.0")); 83 assert(!isValidVersion("1.0 .0")); 84 assert(!isValidVersion("1.0.0 ")); 85 assert(!isValidVersion("1.0.0-a_b")); 86 assert(!isValidVersion("1.0.0+")); 87 assert(!isValidVersion("1.0.0-")); 88 assert(!isValidVersion("1.0.0-+a")); 89 assert(!isValidVersion("1.0.0-a+")); 90 assert(!isValidVersion("1.0")); 91 assert(!isValidVersion("1.0-1.0")); 92 } 93 94 bool isPreReleaseVersion(string ver) 95 in { assert(isValidVersion(ver)); } 96 body { 97 foreach (i; 0 .. 2) { 98 auto di = ver.indexOf('.'); 99 assert(di > 0); 100 ver = ver[di+1 .. $]; 101 } 102 auto di = ver.indexOf('-'); 103 if (di < 0) return false; 104 return isValidNumber(ver[0 .. di]); 105 } 106 107 /** 108 Compares the precedence of two SemVer version strings. 109 110 The version strings must be validated using isValidVersion() before being 111 passed to this function. 112 */ 113 int compareVersions(string a, string b) 114 { 115 // compare a.b.c numerically 116 if (auto ret = compareNumber(a, b)) return ret; 117 assert(a[0] == '.' && b[0] == '.'); 118 a.popFront(); b.popFront(); 119 if (auto ret = compareNumber(a, b)) return ret; 120 assert(a[0] == '.' && b[0] == '.'); 121 a.popFront(); b.popFront(); 122 if (auto ret = compareNumber(a, b)) return ret; 123 124 // give precedence to non-prerelease versions 125 bool apre = a.length > 0 && a[0] == '-'; 126 bool bpre = b.length > 0 && b[0] == '-'; 127 if (apre != bpre) return bpre - apre; 128 if (!apre) return 0; 129 130 // compare the prerelease tail lexicographically 131 do { 132 a.popFront(); b.popFront(); 133 if (auto ret = compareIdentifier(a, b)) return ret; 134 } while (a.length > 0 && b.length > 0 && a[0] != '+' && b[0] != '+'); 135 136 // give longer prerelease tails precedence 137 bool aempty = a.length == 0 || a[0] == '+'; 138 bool bempty = b.length == 0 || b[0] == '+'; 139 if (aempty == bempty) { 140 assert(aempty); 141 return 0; 142 } 143 return bempty - aempty; 144 } 145 146 unittest { 147 void assertLess(string a, string b) { 148 assert(compareVersions(a, b) < 0, "Failed for "~a~" < "~b); 149 assert(compareVersions(b, a) > 0); 150 assert(compareVersions(a, a) == 0); 151 assert(compareVersions(b, b) == 0); 152 } 153 assertLess("1.0.0", "2.0.0"); 154 assertLess("2.0.0", "2.1.0"); 155 assertLess("2.1.0", "2.1.1"); 156 assertLess("1.0.0-alpha", "1.0.0"); 157 assertLess("1.0.0-alpha", "1.0.0-alpha.1"); 158 assertLess("1.0.0-alpha.1", "1.0.0-alpha.beta"); 159 assertLess("1.0.0-alpha.beta", "1.0.0-beta"); 160 assertLess("1.0.0-beta", "1.0.0-beta.2"); 161 assertLess("1.0.0-beta.2", "1.0.0-beta.11"); 162 assertLess("1.0.0-beta.11", "1.0.0-rc.1"); 163 assertLess("1.0.0-rc.1", "1.0.0"); 164 assert(compareVersions("1.0.0", "1.0.0+1.2.3") == 0); 165 assert(compareVersions("1.0.0", "1.0.0+1.2.3-2") == 0); 166 assert(compareVersions("1.0.0+asdasd", "1.0.0+1.2.3") == 0); 167 assertLess("2.0.0", "10.0.0"); 168 assertLess("1.0.0-2", "1.0.0-10"); 169 assertLess("1.0.0-99", "1.0.0-1a"); 170 assertLess("1.0.0-99", "1.0.0-a"); 171 } 172 173 174 private int compareIdentifier(ref string a, ref string b) 175 { 176 bool anumber = true; 177 bool bnumber = true; 178 bool aempty = true, bempty = true; 179 int res = 0; 180 while (true) { 181 if (a.front != b.front && res == 0) res = a.front - b.front; 182 if (anumber && (a.front < '0' || a.front > '9')) anumber = false; 183 if (bnumber && (b.front < '0' || b.front > '9')) bnumber = false; 184 a.popFront(); b.popFront(); 185 aempty = a.empty || a.front == '.' || a.front == '+'; 186 bempty = b.empty || b.front == '.' || b.front == '+'; 187 if (aempty || bempty) break; 188 } 189 190 if (anumber && bnumber) { 191 // the !empty value might be an indentifier instead of a number, but identifiers always have precedence 192 if (aempty != bempty) return bempty - aempty; 193 return res; 194 } else { 195 if (anumber && aempty) return -1; 196 if (bnumber && bempty) return 1; 197 // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)! 198 static assert('0' < 'a' && '0' < 'A'); 199 if (res != 0) return res; 200 return bempty - aempty; 201 } 202 } 203 204 private int compareNumber(ref string a, ref string b) 205 { 206 int res = 0; 207 while (true) { 208 if (a.front != b.front && res == 0) res = a.front - b.front; 209 a.popFront(); b.popFront(); 210 auto aempty = a.empty || (a.front < '0' || a.front > '9'); 211 auto bempty = b.empty || (b.front < '0' || b.front > '9'); 212 if (aempty != bempty) return bempty - aempty; 213 if (aempty) return res; 214 } 215 } 216 217 private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false) 218 { 219 if (str.length == 0) return false; 220 while (str.length) { 221 auto end = str.indexOf('.'); 222 if (end < 0) end = str.length; 223 if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false; 224 if (end < str.length) str = str[end+1 .. $]; 225 else break; 226 } 227 return true; 228 } 229 230 private bool isValidIdentifier(string str, bool allow_leading_zeros = false) 231 { 232 if (str.length < 1) return false; 233 234 bool numeric = true; 235 foreach (ch; str) { 236 switch (ch) { 237 default: return false; 238 case 'a': .. case 'z': 239 case 'A': .. case 'Z': 240 case '-': 241 numeric = false; 242 break; 243 case '0': .. case '9': 244 break; 245 } 246 } 247 248 if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false; 249 250 return true; 251 } 252 253 private bool isValidNumber(string str) 254 { 255 if (str.length < 1) return false; 256 foreach (ch; str) 257 if (ch < '0' || ch > '9') 258 return false; 259 260 // don't allow leading zeros 261 if (str[0] == '0' && str.length > 1) return false; 262 263 return true; 264 } 265 266 private sizediff_t indexOfAny(string str, in char[] chars) 267 { 268 sizediff_t ret = -1; 269 foreach (ch; chars) { 270 auto idx = str.indexOf(ch); 271 if (idx >= 0 && (ret < 0 || idx < ret)) 272 ret = idx; 273 } 274 return ret; 275 }