sync-constants.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #!/usr/bin/env php
  2. <?php
  3. /**
  4. * This script checks the constants defined in the curl PHP extension, against those documented on the cURL website:
  5. * https://curl.haxx.se/libcurl/c/symbols-in-versions.html
  6. *
  7. * See the discussion at: https://github.com/php/php-src/pull/2961
  8. */
  9. const CURL_DOC_FILE = 'https://curl.haxx.se/libcurl/c/symbols-in-versions.html';
  10. const SOURCE_FILE = __DIR__ . '/interface.c';
  11. const MIN_SUPPORTED_CURL_VERSION = '7.29.0';
  12. const IGNORED_CONSTANTS = [
  13. 'CURLOPT_PROGRESSDATA'
  14. ];
  15. const CONSTANTS_REGEX_PATTERN = '~^CURL(?:OPT|_VERSION)_[A-Z0-9_]+$~';
  16. $curlConstants = getCurlConstants();
  17. $sourceConstants = getSourceConstants();
  18. $notInPHP = []; // In cURL doc, but missing from PHP
  19. $notInCurl = []; // In the PHP source, but not in the cURL doc
  20. $outdated = []; // In the PHP source, but removed before the minimum supported cURL version
  21. foreach ($curlConstants as $name => [$introduced, $deprecated, $removed]) {
  22. $inPHP = in_array($name, $sourceConstants);
  23. if ($removed !== null) {
  24. if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) <= 0) {
  25. // constant removed before the minimum supported version
  26. continue;
  27. }
  28. }
  29. if (! $inPHP) {
  30. $notInPHP[$name] = [$introduced, $removed];
  31. }
  32. }
  33. foreach ($sourceConstants as $name) {
  34. if (! isset($curlConstants[$name])) {
  35. $notInCurl[] = $name;
  36. continue;
  37. }
  38. $removed = $curlConstants[$name][2];
  39. if ($removed === null) {
  40. continue;
  41. }
  42. if (version_compare($removed, MIN_SUPPORTED_CURL_VERSION) <= 0) {
  43. // constant removed before the minimum supported version
  44. $outdated[$name] = $removed;
  45. }
  46. }
  47. $allGood = true;
  48. if ($notInPHP) {
  49. uasort($notInPHP, function($a, $b) {
  50. return version_compare($a[0], $b[0]);
  51. });
  52. $table = new AsciiTable();
  53. $table->add('Constant', 'Introduced', '', 'Removed', '');
  54. foreach ($notInPHP as $name => [$introduced, $removed]) {
  55. if ($removed === null) {
  56. $removed = '';
  57. $removedHex = '';
  58. } else {
  59. $removedHex = getHexVersion($removed);
  60. }
  61. $table->add($name, $introduced, getHexVersion($introduced), $removed, $removedHex);
  62. }
  63. echo "Constants missing from the PHP source:\n\n";
  64. echo $table, "\n";
  65. $allGood = false;
  66. }
  67. if ($notInCurl) {
  68. $table = new AsciiTable();
  69. foreach ($notInCurl as $name) {
  70. $table->add($name);
  71. }
  72. echo "Constants defined in the PHP source, but absent from the cURL documentation:\n\n";
  73. echo $table, "\n";
  74. $allGood = false;
  75. }
  76. if ($outdated) {
  77. uasort($outdated, function($a, $b) {
  78. return version_compare($a, $b);
  79. });
  80. $table = new AsciiTable();
  81. $table->add('Constant', 'Removed');
  82. foreach ($outdated as $name => $version) {
  83. $table->add($name, $version);
  84. }
  85. echo "Constants defined in the PHP source, but removed before the minimum supported cURL version:\n\n";
  86. echo $table, "\n";
  87. $allGood = false;
  88. }
  89. if ($allGood) {
  90. echo "All good! Source code and cURL documentation are in sync.\n";
  91. }
  92. /**
  93. * Loads and parses the cURL constants from the online documentation.
  94. *
  95. * The result is an associative array where the key is the constant name, and the value is a numeric array with:
  96. * - the introduced version
  97. * - the deprecated version (nullable)
  98. * - the removed version (nullable)
  99. *
  100. * @return array
  101. */
  102. function getCurlConstants() : array
  103. {
  104. $html = file_get_contents(CURL_DOC_FILE);
  105. // Extract the constant list from the HTML file (located in the only <pre> tag in the page)
  106. preg_match('~<pre>([^<]+)</pre>~', $html, $matches);
  107. $constantList = $matches[1];
  108. /**
  109. * Parse the cURL constant lines. Possible formats:
  110. *
  111. * Name Introduced Deprecated Removed
  112. * CURLOPT_CRLFILE 7.19.0
  113. * CURLOPT_DNS_USE_GLOBAL_CACHE 7.9.3 7.11.1
  114. * CURLOPT_FTPASCII 7.1 7.11.1 7.15.5
  115. * CURLOPT_HTTPREQUEST 7.1 - 7.15.5
  116. */
  117. $regexp = '/^([A-Za-z0-9_]+) +([0-9\.]+)(?: +([0-9\.\-]+))?(?: +([0-9\.]+))?/m';
  118. preg_match_all($regexp, $constantList, $matches, PREG_SET_ORDER);
  119. $constants = [];
  120. foreach ($matches as $match) {
  121. $name = $match[1];
  122. $introduced = $match[2];
  123. $deprecated = $match[3] ?? null;
  124. $removed = $match[4] ?? null;
  125. if (in_array($name, IGNORED_CONSTANTS, true) || !preg_match(CONSTANTS_REGEX_PATTERN, $name)) {
  126. // not a wanted constant
  127. continue;
  128. }
  129. if ($deprecated === '-') { // deprecated version can be a hyphen
  130. $deprecated = null;
  131. }
  132. $constants[$name] = [$introduced, $deprecated, $removed];
  133. }
  134. return $constants;
  135. }
  136. /**
  137. * Parses the defined cURL constants from the PHP extension source code.
  138. *
  139. * The result is a numeric array whose values are the constant names.
  140. *
  141. * @return array
  142. */
  143. function getSourceConstants() : array
  144. {
  145. $source = file_get_contents(SOURCE_FILE);
  146. preg_match_all('/REGISTER_CURL_CONSTANT\(([A-Za-z0-9_]+)\)/', $source, $matches);
  147. $constants = [];
  148. foreach ($matches[1] as $name) {
  149. if ($name === '__c') { // macro
  150. continue;
  151. }
  152. if (!preg_match(CONSTANTS_REGEX_PATTERN, $name)) {
  153. // not a wanted constant
  154. continue;
  155. }
  156. $constants[] = $name;
  157. }
  158. return $constants;
  159. }
  160. /**
  161. * Converts a version number to its hex representation as used in the extension source code.
  162. *
  163. * Example: 7.25.1 => 0x071901
  164. *
  165. * @param string $version
  166. *
  167. * @return string
  168. *
  169. * @throws \RuntimeException
  170. */
  171. function getHexVersion(string $version) : string
  172. {
  173. $parts = explode('.', $version);
  174. if (count($parts) === 2) {
  175. $parts[] = '0';
  176. }
  177. if (count($parts) !== 3) {
  178. throw new \RuntimeException('Invalid version number: ' . $version);
  179. }
  180. $hex = '0x';
  181. foreach ($parts as $value) {
  182. if (! ctype_digit($value) || strlen($value) > 3) {
  183. throw new \RuntimeException('Invalid version number: ' . $version);
  184. }
  185. $value = (int) $value;
  186. if ($value > 255) {
  187. throw new \RuntimeException('Invalid version number: ' . $version);
  188. }
  189. $value = dechex($value);
  190. if (strlen($value) === 1) {
  191. $value = '0' . $value;
  192. }
  193. $hex .= $value;
  194. }
  195. return $hex;
  196. }
  197. /**
  198. * A simple helper to create ASCII tables.
  199. * It assumes that the same number of columns is always given to add().
  200. */
  201. class AsciiTable
  202. {
  203. /**
  204. * @var array
  205. */
  206. private $values = [];
  207. /**
  208. * @var array
  209. */
  210. private $length = [];
  211. /**
  212. * @var int
  213. */
  214. private $padding = 4;
  215. /**
  216. * @param string[] $values
  217. *
  218. * @return void
  219. */
  220. public function add(string ...$values) : void
  221. {
  222. $this->values[] = $values;
  223. foreach ($values as $key => $value) {
  224. $length = strlen($value);
  225. if (isset($this->length[$key])) {
  226. $this->length[$key] = max($this->length[$key], $length);
  227. } else {
  228. $this->length[$key] = $length;
  229. }
  230. }
  231. }
  232. /**
  233. * @return string
  234. */
  235. public function __toString() : string
  236. {
  237. $result = '';
  238. foreach ($this->values as $values) {
  239. foreach ($values as $key => $value) {
  240. if ($key !== 0) {
  241. $result .= str_repeat(' ', $this->padding);
  242. }
  243. $result .= str_pad($value, $this->length[$key]);
  244. }
  245. $result .= "\n";
  246. }
  247. return $result;
  248. }
  249. }