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