sync-constants.php 7.6 KB

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