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