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