Newer
Older
dub_jkp / source / dub / semver.d
@Sönke Ludwig Sönke Ludwig on 20 Apr 2016 11 KB Review dub.semver and improve documentation.
  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.  
  24. /**
  25. Validates a version string according to the SemVer specification.
  26. */
  27. bool isValidVersion(string ver)
  28. {
  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)
  105. in { assert(isValidVersion(ver)); }
  106. body {
  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. {
  139. // compare a.b.c numerically
  140. if (auto ret = compareNumber(a, b)) return ret;
  141. assert(a[0] == '.' && b[0] == '.');
  142. a.popFront(); b.popFront();
  143. if (auto ret = compareNumber(a, b)) return ret;
  144. assert(a[0] == '.' && b[0] == '.');
  145. a.popFront(); b.popFront();
  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.popFront(); b.popFront();
  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. // Cut off metadata and prerelease information.
  229. auto mi = ver.indexOfAny("+-");
  230. if (mi > 0) ver = ver[0..mi];
  231. // Increment next to last version from a[.b[.c]].
  232. auto splitted = split(ver, ".");
  233. assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
  234. auto to_inc = splitted.length == 3? 1 : 0;
  235. splitted = splitted[0 .. to_inc+1];
  236. splitted[to_inc] = to!string(to!int(splitted[to_inc]) + 1);
  237. // Fill up to three compontents to make valid SemVer version.
  238. while (splitted.length < 3) splitted ~= "0";
  239. return splitted.join(".");
  240. }
  241. ///
  242. unittest {
  243. assert("1.0.0" == bumpVersion("0"));
  244. assert("1.0.0" == bumpVersion("0.0"));
  245. assert("0.1.0" == bumpVersion("0.0.0"));
  246. assert("1.3.0" == bumpVersion("1.2.3"));
  247. assert("1.3.0" == bumpVersion("1.2.3+metadata"));
  248. assert("1.3.0" == bumpVersion("1.2.3-pre.release"));
  249. assert("1.3.0" == bumpVersion("1.2.3-pre.release+metadata"));
  250. }
  251.  
  252. /**
  253. Takes a partial version and expands it to a valid SemVer version.
  254. This function corresponds to the semantivs of the "~>" comparison operator's
  255. lower bound.
  256.  
  257. See_Also: `bumpVersion`
  258. */
  259. string expandVersion(string ver) {
  260. auto mi = ver.indexOfAny("+-");
  261. auto sub = "";
  262. if (mi > 0) {
  263. sub = ver[mi..$];
  264. ver = ver[0..mi];
  265. }
  266. auto splitted = split(ver, ".");
  267. assert(splitted.length > 0 && splitted.length <= 3, "Version corrupt: " ~ ver);
  268. while (splitted.length < 3) splitted ~= "0";
  269. return splitted.join(".") ~ sub;
  270. }
  271. ///
  272. unittest {
  273. assert("1.0.0" == expandVersion("1"));
  274. assert("1.0.0" == expandVersion("1.0"));
  275. assert("1.0.0" == expandVersion("1.0.0"));
  276. // These are rather excotic variants...
  277. assert("1.0.0-pre.release" == expandVersion("1-pre.release"));
  278. assert("1.0.0+meta" == expandVersion("1+meta"));
  279. assert("1.0.0-pre.release+meta" == expandVersion("1-pre.release+meta"));
  280. }
  281.  
  282. private int compareIdentifier(ref string a, ref string b)
  283. {
  284. bool anumber = true;
  285. bool bnumber = true;
  286. bool aempty = true, bempty = true;
  287. int res = 0;
  288. while (true) {
  289. if (a.front != b.front && res == 0) res = a.front - b.front;
  290. if (anumber && (a.front < '0' || a.front > '9')) anumber = false;
  291. if (bnumber && (b.front < '0' || b.front > '9')) bnumber = false;
  292. a.popFront(); b.popFront();
  293. aempty = a.empty || a.front == '.' || a.front == '+';
  294. bempty = b.empty || b.front == '.' || b.front == '+';
  295. if (aempty || bempty) break;
  296. }
  297.  
  298. if (anumber && bnumber) {
  299. // the !empty value might be an indentifier instead of a number, but identifiers always have precedence
  300. if (aempty != bempty) return bempty - aempty;
  301. return res;
  302. } else {
  303. if (anumber && aempty) return -1;
  304. if (bnumber && bempty) return 1;
  305. // this assumption is necessary to correctly classify 111A > 11111 (ident always > number)!
  306. static assert('0' < 'a' && '0' < 'A');
  307. if (res != 0) return res;
  308. return bempty - aempty;
  309. }
  310. }
  311.  
  312. private int compareNumber(ref string a, ref string b)
  313. {
  314. int res = 0;
  315. while (true) {
  316. if (a.front != b.front && res == 0) res = a.front - b.front;
  317. a.popFront(); b.popFront();
  318. auto aempty = a.empty || (a.front < '0' || a.front > '9');
  319. auto bempty = b.empty || (b.front < '0' || b.front > '9');
  320. if (aempty != bempty) return bempty - aempty;
  321. if (aempty) return res;
  322. }
  323. }
  324.  
  325. private bool isValidIdentifierChain(string str, bool allow_leading_zeros = false)
  326. {
  327. if (str.length == 0) return false;
  328. while (str.length) {
  329. auto end = str.indexOf('.');
  330. if (end < 0) end = str.length;
  331. if (!isValidIdentifier(str[0 .. end], allow_leading_zeros)) return false;
  332. if (end < str.length) str = str[end+1 .. $];
  333. else break;
  334. }
  335. return true;
  336. }
  337.  
  338. private bool isValidIdentifier(string str, bool allow_leading_zeros = false)
  339. {
  340. if (str.length < 1) return false;
  341.  
  342. bool numeric = true;
  343. foreach (ch; str) {
  344. switch (ch) {
  345. default: return false;
  346. case 'a': .. case 'z':
  347. case 'A': .. case 'Z':
  348. case '-':
  349. numeric = false;
  350. break;
  351. case '0': .. case '9':
  352. break;
  353. }
  354. }
  355.  
  356. if (!allow_leading_zeros && numeric && str[0] == '0' && str.length > 1) return false;
  357.  
  358. return true;
  359. }
  360.  
  361. private bool isValidNumber(string str)
  362. {
  363. if (str.length < 1) return false;
  364. foreach (ch; str)
  365. if (ch < '0' || ch > '9')
  366. return false;
  367.  
  368. // don't allow leading zeros
  369. if (str[0] == '0' && str.length > 1) return false;
  370.  
  371. return true;
  372. }
  373.  
  374. private sizediff_t indexOfAny(string str, in char[] chars)
  375. {
  376. sizediff_t ret = -1;
  377. foreach (ch; chars) {
  378. auto idx = str.indexOf(ch);
  379. if (idx >= 0 && (ret < 0 || idx < ret))
  380. ret = idx;
  381. }
  382. return ret;
  383. }